DEV Community: Pedro SandersThe latest articles on DEV Community by Pedro Sanders (@psanders).
https://dev.to/psanders
https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F484575%2F282dda12-22d4-44f9-8dbb-2341a30da96b.jpgDEV Community: Pedro Sanders
https://dev.to/psanders
enBuilding a VoIP Network with Routr on DigitalOcean Kubernetes: Part IPedro SandersMon, 04 Mar 2024 12:43:15 +0000
https://dev.to/fonoster/building-a-voip-network-with-routr-on-digitalocean-kubernetes-part-i-f8n
https://dev.to/fonoster/building-a-voip-network-with-routr-on-digitalocean-kubernetes-part-i-f8n<p>Building a VoIP Network with Routr on DigitalOcean Kubernetes: Part I</p>
<p>Routr v2 takes a radical design approach by being container-first and taking full advantage of environments like Kubernetes. In this three-part tutorial, you will learn how to:</p>
<ul>
<li>Create a load balancer on DOKS and deploy Routr</li>
<li>Secure the admin and signaling ports with Let's Encrypt</li>
<li>Prepare the network for production</li>
</ul>
<p>Before you continue, be sure to star Routr on GitHub 👇</p>
<div class="ltag-github-readme-tag">
<div class="readme-overview">
<h2>
<img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo">
<a href="proxy.php?url=https://github.com/fonoster" rel="noopener noreferrer">
fonoster
</a> / <a href="proxy.php?url=https://github.com/fonoster/routr" rel="noopener noreferrer">
routr
</a>
</h2>
<h3>
⚡ The future of programmable SIP servers.
</h3>
</div>
</div>
<h2>
Requirements
</h2>
<p>Before you start this tutorial, you will need the following:</p>
<ul>
<li>An account in DigitalOcean and a DOKS cluster</li>
<li>NodeJS >= 18 (Use nvm if possible)</li>
<li>Routr command-line tool (Install it with <code>npm install -g @routr/ctl</code>)</li>
<li>Kubectl with access to your DOKS cluster</li>
<li>Helm (Get from here <a href="proxy.php?url=https://helm.sh/" rel="noopener noreferrer">https://helm.sh/</a>)</li>
</ul>
<h2>
Check the DOKS cluster and other dependencies
</h2>
<p>This tutorial assumes you have a running DOKS cluster. If you need help creating a DOKS creating, please visit the following link: <a href="proxy.php?url=https://docs.digitalocean.com/products/kubernetes/how-to/create-clusters/" rel="noopener noreferrer">https://docs.digitalocean.com/products/kubernetes/how-to/create-clusters/</a></p>
<p>With that, ensure you can connect to your cluster using Kubectl before continuing with the next steps.</p>
<h2>
Creating a new Load Balancer in Digital Ocean
</h2>
<p>To create a new load balancer, go to the networking section within the DigitalOcean panel, find the "Load Balancers" tab, and click "Create Load Balancer."</p>
<p>Your page will look like this:</p>
<p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9wclt4swy9bx84agevie.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9wclt4swy9bx84agevie.png" alt="Load Balancers Page"></a></p>
<p>Follow the creation steps using all the default values and note the load balancer's name.</p>
<h2>
Installing Routr using Helm
</h2>
<p>Once your load balancer is up and running, follow the next instructions to install Routr with Helm.</p>
<p>First, add Routr to the Helm repository with:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
helm repo add routr https://routr.io/charts
helm repo update
</code></pre>
</div>
<p>You can get details about the new repo with the following:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
helm repo list
</code></pre>
</div>
<p>Then, create a namespace for Routr using the create namespace subcommand:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
kubectl create namespace routr
</code></pre>
</div>
<p>Next, create a <code>values.yaml</code> with the following content find, and remember to replace the externalAddrs property and serviceAnnotationsTCP with your values.</p>
<p>Filename: <em>values.yaml</em></p>
<div class="highlight js-code-highlight">
<pre class="highlight yaml"><code>
<span class="na">edgeport</span><span class="pi">:</span>
<span class="na">externalAddrs</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">159.203.158.25"</span><span class="pi">]</span>
<span class="na">serviceTypeTCP</span><span class="pi">:</span> <span class="s">LoadBalancer</span>
<span class="na">serviceAnnotationsTCP</span><span class="pi">:</span>
<span class="na">service.beta.kubernetes.io/do-loadbalancer-name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">nyc1-load-balancer-01"</span>
<span class="na">transport</span><span class="pi">:</span>
<span class="na">tcp</span><span class="pi">:</span>
<span class="na">enabled</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre>
</div>
<p>Alternatively, you could use the annotation <code>kubernetes.digitalocean.com/load-balancer-id</code> to use the load balancer's ID instead of the name.</p>
<p>Finally, create the SIP network with:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
helm <span class="nb">install </span>sipnet routr/routr-connect <span class="nt">--namespace</span> routr <span class="nt">-f</span> values.yaml
</code></pre>
</div>
<p>Within a few minutes, you will see the following message:</p>
<p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frrnr1r4t9yogqogiwake.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frrnr1r4t9yogqogiwake.png" alt="Succesful deployment"></a></p>
<p>You can check the status of the Pods with the "get pods" subcommand, like here:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
kubectl get pods <span class="nt">-n</span> routr
</code></pre>
</div>
<p>You should see a list of pods and their status. If the status of all services is "Running," you are ready to go.</p>
<p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fko9h0ux51d154tpax2s6.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fko9h0ux51d154tpax2s6.png" alt="PODs status"></a></p>
<p>Please see the <a href="proxy.php?url=https://github.com/fonoster/routr/blob/main/ops/charts/connect/README.md" rel="noopener noreferrer">Official Chart</a> for many more options for your deployment.</p>
<h2>
Configuring a Domain, Credentials, and Agents in Routr
</h2>
<p>With Routr deployed, the next step will be to create the following resources:</p>
<ul>
<li>The "sip.local" domain</li>
<li>Two sets of credentials (e.g., 1001 and 1002)</li>
<li>Two SIP agents (e.g., John and Jane)</li>
</ul>
<p>To send admin commands to Routr, you must expose the admin port.</p>
<p>Here, for simplicity, we will use <code>port-forward</code> to expose the admin port. However, you might use a different method in production, like creating a separate load balancer.</p>
<p>To open the admin port, first list the services of your deployment and find the one for your admin port with the following:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
kubectl get services <span class="nt">-n</span> routr
</code></pre>
</div>
<p>Then, open the admin using the port-forward subcommand:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
kubectl port-forward svc/sipnet-routr-apiserver 51907 <span class="nt">-n</span> routr
</code></pre>
</div>
<p>Once the port is open, on a separate screen, begin creating a Domain with:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
rctl domains create <span class="nt">--insecure</span> <span class="nt">--endpoint</span><span class="o">=</span>localhost:51907
</code></pre>
</div>
<p>Your output will look as follows:</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>
Press ^C at any time to quit.
› Warning: Egress rules unavailable due to 0 configured numbers.
? Friendly Name Local Domain
? SIP URI sip.local
? IP Access Control List None
? Ready? Yes
Creating Domain Local Domain... 3b20410a-3c80-4f66-b7b3-58f65ff65352
</code></pre>
</div>
<p>In the next part of the series, I will go over securing the connection with a self-signed certificate or using certificates from Let's Encrypt.</p>
<p>Continue by creating two sets of credentials with the following command:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
rctl credentials create <span class="nt">--insecure</span> <span class="nt">--endpoint</span><span class="o">=</span>localhost:51907
</code></pre>
</div>
<p>Follow the prompt and repeat to create two sets (e.g., 1001 and 1002). Your output for the first credential will look like this:</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>
This utility will help you create a new set of Credentials.
Press ^C at any time to quit.
? Friendly Name John Doe - Credentials
? Username 1001
? Password [hidden]
? Ready? Yes
Creating Credentials John Doe - Credentials... 5fbc7367-a59d-4555-9fc4-a15ff29c24c8
</code></pre>
</div>
<p>Finally, create two sets of Agents:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
rctl agents create <span class="nt">--insecure</span> <span class="nt">--endpoint</span><span class="o">=</span>localhost:51907
</code></pre>
</div>
<p>The output for your first Agent will look like this:</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>
This utility will help you create a new Agent.
Press ^C at any time to quit.
? Friendly Name John Doe
? Select a Domain sip.local
? Username 1001
? Credentials Name John Doe - Credentials
? Max Contacts
? Privacy None
? Enabled? Yes
? Ready? Yes
Creating Agent John Doe... 662a379d-66f1-4e6e-9df5-5126f1dcb930
</code></pre>
</div>
<p>Please repeat the process for Jane.</p>
<h2>
Registering and Calling using Blink
</h2>
<p>Using Blink or your preferred softphone, create a new agent using the parameters in the abovementioned steps.</p>
<p>The result should show a green indicator like in the screenshot below.</p>
<p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsgcxqzjut3q7g0yf2yzz.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsgcxqzjut3q7g0yf2yzz.png" alt="Registering Blink"></a></p>
<p>Repeat the process from another instance of Blink, and make a call going to the main screen and typing the extension.</p>
<p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fil71x7byty7g4u323mk5.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fil71x7byty7g4u323mk5.png" alt="Calling the extension 1002"></a></p>
<p>You should be able to send and receive calls from both devices.</p>
<h2>
What's next
</h2>
<p>Thank you for making it this far. In the upcoming days, I will publish Part II of this series, which will go over securing the signaling path and the admin port. In the meantime, feel free to enjoy the following tutorials:</p>
<ul>
<li><a href="proxy.php?url=https://dev.to/fonoster/building-scalable-ivrs-for-businesses-with-routr-and-asterisk-cjd">Building scalable IVRs for businesses with Routr and Asterisk</a></li>
<li><a href="proxy.php?url=https://dev.to/fonoster/browser-to-browser-calling-with-sipjs-and-routr-3l07">Browser-to-Browser calling with SIP.js and Routr</a></li>
<li><a href="proxy.php?url=https://dev.to/psanders/simplify-sipjs-security-with-short-lived-tokens-mb8">Simplify SIP.js security with short-lived tokens</a></li>
</ul>
routrvoipkubernetessipSimplify SIP.js security with short-lived tokensPedro SandersMon, 26 Feb 2024 21:24:19 +0000
https://dev.to/fonoster/simplify-sipjs-security-with-short-lived-tokens-mb8
https://dev.to/fonoster/simplify-sipjs-security-with-short-lived-tokens-mb8<p>Routr v2 has introduced Ephemeral Agents, eliminating the need to store sensitive credentials for WebRTC clients like SIP.js and using short-lived JWT tokens instead.</p>
<p>This tutorial will guide you through creating private and public keys and JWT tokens and configuring Routr to validate the tokens using the public key.</p>
<div class="ltag-github-readme-tag">
<div class="readme-overview">
<h2>
<img src="proxy.php?url=https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo">
<a href="proxy.php?url=https://github.com/fonoster">
fonoster
</a> / <a href="proxy.php?url=https://github.com/fonoster/routr">
routr
</a>
</h2>
<h3>
⚡ The future of programmable SIP servers.
</h3>
</div>
</div>
<h2>
Requirements
</h2>
<p>Before you start this tutorial, you will need the following:</p>
<ul>
<li>Docker Engine installed on your computer or the cloud</li>
<li>NodeJS 18+ (Use nvm if possible)</li>
<li>Routr command-line tool (Install it with npm install -g @routr/ctl)</li>
</ul>
<h2>
Creating a private and public key
</h2>
<p>To create a JWT token, you must first create a set of private and public keys. The private key is needed to sign the JWT token, and the public key is to verify the JWT token's signature.</p>
<p>You can create a private key using the following command:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="nb">mkdir</span> <span class="nt">-p</span> voipnet/work
<span class="nb">cd </span>voipnet/work
openssl genpkey <span class="nt">-algorithm</span> RSA <span class="nt">-out</span> private.key <span class="nt">-pkeyopt</span> rsa_keygen_bits:4096
</code></pre>
</div>
<blockquote>
<p>This command will create a new key named private.key. Keep this key safe, as it will be used to sign new JWT tokens.</p>
</blockquote>
<p>With the private key in place, you can now create the public key with:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>openssl rsa <span class="nt">-in</span> private.key <span class="nt">-pubout</span> <span class="nt">-out</span> public.key
</code></pre>
</div>
<p>The previous command resulted in a new key named public.key, which is necessary to verify the claims in the JWT token.</p>
<h2>
Creating a JWT token
</h2>
<p>In this section, you will create a small NodeJS script using the private key to sign a JWT token.</p>
<p>Now that you have a pair of private and public keys, you can create a JWT token. To create a JWT token, first, initialize a new Node project with:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>npm init <span class="nt">-y</span>
</code></pre>
</div>
<p>Then, install the <code>jsonwebtoken</code> package as follows:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>npm <span class="nb">install </span>jsonwebtoken
</code></pre>
</div>
<p>Next, create a file named create-jwt.js with the following content:</p>
<p>Filename: <em>voipnet/work/create-jwt.js</em><br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">fs</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">jwt</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">jsonwebtoken</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">privateKey</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nf">readFileSync</span><span class="p">(</span><span class="dl">"</span><span class="s2">private.key</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">fs</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">jwt</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">jsonwebtoken</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">privateKey</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nf">readFileSync</span><span class="p">(</span><span class="dl">"</span><span class="s2">private.key</span><span class="dl">"</span><span class="p">);</span>
<span class="c1">// Epheperal Agent's claims</span>
<span class="kd">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">ref</span><span class="p">:</span> <span class="dl">"</span><span class="s2">agent-01</span><span class="dl">"</span><span class="p">,</span>
<span class="na">domainRef</span><span class="p">:</span> <span class="dl">"</span><span class="s2">domain-01</span><span class="dl">"</span><span class="p">,</span>
<span class="na">aor</span><span class="p">:</span> <span class="dl">"</span><span class="s2">sip:[email protected]</span><span class="dl">"</span><span class="p">,</span>
<span class="na">aorLink</span><span class="p">:</span> <span class="dl">"</span><span class="s2">sip:[email protected]</span><span class="dl">"</span><span class="p">,</span>
<span class="na">domain</span><span class="p">:</span> <span class="dl">"</span><span class="s2">sip.local</span><span class="dl">"</span><span class="p">,</span>
<span class="na">privacy</span><span class="p">:</span> <span class="dl">"</span><span class="s2">NONE</span><span class="dl">"</span><span class="p">,</span>
<span class="na">allowedMethods</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">INVITE</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">REGISTER</span><span class="dl">"</span><span class="p">]</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">signOptions</span> <span class="o">=</span> <span class="p">{</span> <span class="na">expiresIn</span><span class="p">:</span> <span class="dl">"</span><span class="s2">1h</span><span class="dl">"</span><span class="p">,</span> <span class="na">algorithm</span><span class="p">:</span> <span class="dl">"</span><span class="s2">RS256</span><span class="dl">"</span> <span class="p">};</span>
<span class="kd">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="nx">jwt</span><span class="p">.</span><span class="nf">sign</span><span class="p">(</span><span class="nx">payload</span><span class="p">,</span> <span class="nx">privateKey</span><span class="p">,</span> <span class="nx">signOptions</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Token: </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">token</span><span class="p">);</span>
</code></pre>
</div>
<p>As you can see in the previous script, the JWT token assigns claims to the Ephemeral Agent. Incidentally, the claims are similar to the values used by Routr when configuring a regular agent.</p>
<blockquote>
<p>Please note that while you can use an arbitrary <code>aor</code>, the <code>domainRef</code> and <code>domain</code> claims must match a domain in the server.</p>
</blockquote>
<p>Finally, run the following script to generate a fresh JWT token:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>node create-jwt.js
</code></pre>
</div>
<p>After running the previous script, you will get a JWT token with an output similar to this:</p>
<p><a href="proxy.php?url=https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl4gzhq2yufkkwuw68btk.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl4gzhq2yufkkwuw68btk.png" alt="Image with an example JWT token" width="800" height="387"></a></p>
<h2>
Deploying Routr with Docker
</h2>
<p>You will use Docker Compose for this setup. If you don't have Docker installed, download it from the official website. Once you have Docker, create a new file named compose.yaml with the following content:</p>
<p>Filename: <em>voipnet/compose.yaml</em><br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight yaml"><code><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3"</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">routr</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">fonoster/routr-one:latest</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="na">EXTERNAL_ADDRS</span><span class="pi">:</span> <span class="s">${DOCKER_HOST_ADDRESS}</span>
<span class="na">CONNECT_VERIFIER_PUBLIC_KEY_PATH</span><span class="pi">:</span> <span class="s">/keys/public.key</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">51908:51908</span>
<span class="pi">-</span> <span class="s">5062:5062</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="c1"># Makes sure that the data survives container restarts</span>
<span class="pi">-</span> <span class="s">shared:/var/lib/postgresql/data</span>
<span class="pi">-</span> <span class="s">./work/public.key:/keys/public.key</span>
<span class="na">simplephone</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">psanders/simplephone:latest</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">8080:8080</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="na">shared</span><span class="pi">:</span>
</code></pre>
</div>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># NOTE: Be sure to update the IP address</span>
<span class="nv">DOCKER_HOST_ADDRESS</span><span class="o">=</span>192.168.1.7 docker compose up
</code></pre>
</div>
<p>You are all set and need a few seconds for the containers to initialize.</p>
<h2>
Create a new Domain in Routr
</h2>
<p>You will need to create a new Domain using the Routr command-line tool. Issue the following command and follow the prompt:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>rctl domains create <span class="nt">--insecure</span>
</code></pre>
</div>
<p>Notice the <code>--insecure</code> flag, which is required since we don't have a TLS certificate.</p>
<p>The output of your command will look similar to the output below:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>Press ^C at any time to quit.
› Warning: Egress rules unavailable due to 0 configured numbers.
? Friendly Name Local Domain
? SIP URI sip.local
? IP Access Control List None
? Ready? Yes
Creating Domain Local Domain... 3b20410a-3c80-4f66-b7b3-58f65ff65352
</code></pre>
</div>
<p>You might verify the Domain's configuration by running the following command:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>rctl domains get <span class="nt">--insecure</span> <span class="nt">--extended</span>
</code></pre>
</div>
<h2>
Registering a SIP.js client using the JWT token
</h2>
<p>Finally, configure your WebRTC client to register to the server using the claims in the JWT token by adding the <code>X-Connect-Token</code> with the value of the token we created previously.</p>
<p>Here is an example of a SIP.js client using the new JWT token to register to Routr:</p>
<p><a href="proxy.php?url=https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6jjqmp9q16jl8ybe5o0d.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6jjqmp9q16jl8ybe5o0d.png" alt="Example of SIP.js using the X-Connect-Token" width="800" height="454"></a></p>
<h2>
What's next?
</h2>
<p>This tutorial taught you how to use Ephemeral Agents to secure SIP.js clients. You created a JWT token and used it to register a SIP.js client to a Routr server. You also learned how to create private and public keys and deploy Routr with Docker. </p>
<p>Please comment if you find this tutorial helpful, and check out the following related tutorials:</p>
<ul>
<li><a href="proxy.php?url=https://dev.to/fonoster/calling-a-door-phone-from-a-web-browser-1bko">Calling a door phone from a web browser</a></li>
<li><a href="proxy.php?url=https://dev.to/fonoster/building-scalable-ivrs-for-businesses-with-routr-and-asterisk-cjd">Building scalable IVRs for businesses with Routr and Asterisk</a></li>
<li><a href="proxy.php?url=https://dev.to/fonoster/browser-to-browser-calling-with-sipjs-and-routr-3l07">Browser-to-Browser calling with SIP.js and Routr</a></li>
</ul>
routrwebrtcsipjsjavascriptBuilding scalable IVRs for businesses with Routr and AsteriskPedro SandersSun, 25 Feb 2024 20:12:37 +0000
https://dev.to/fonoster/building-scalable-ivrs-for-businesses-with-routr-and-asterisk-cjd
https://dev.to/fonoster/building-scalable-ivrs-for-businesses-with-routr-and-asterisk-cjd<p>In this tutorial, I will share how to create a VoIP network combining Routr, RTPEngine, and Asterisk for highly available voicemail, conference, IVR, or any other application in Asterisk.</p>
<p>The added advantage of this setup is that you can use Asterisk as a feature server and have dedicated instances for each feature you would like to provide.</p>
<div class="ltag-github-readme-tag">
<div class="readme-overview">
<h2>
<img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo">
<a href="proxy.php?url=https://github.com/fonoster" rel="noopener noreferrer">
fonoster
</a> / <a href="proxy.php?url=https://github.com/fonoster/routr" rel="noopener noreferrer">
routr
</a>
</h2>
<h3>
⚡ The future of programmable SIP servers.
</h3>
</div>
</div>
<h2>
Requirements
</h2>
<p>Before you start this tutorial, you will need the following:</p>
<ul>
<li>Docker Engine installed on your computer or the cloud</li>
<li>NodeJS 18+ (Use nvm if possible)</li>
<li>Routr command-line tool (Install it with <code>npm install -g @routr/ctl</code>)</li>
</ul>
<h2>
Deploying Routr and Asterisk using Docker Compose
</h2>
<p>You will use Docker Compose for this setup; however, I recommend using Docker Swarm mode or Kubernetes for a production-ready setup.</p>
<blockquote>
<p>Please see Routr's Helm chart for details on how to run Routr in K8s</p>
</blockquote>
<p>To get started, first create a folder named voipnet. In the new folder, create a file name compose.yaml with the following content:</p>
<p>Filename: <em>voipnet/compose.yaml</em><br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight yaml"><code><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3"</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">routr</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">fonoster/routr-one:latest</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="na">EXTERNAL_ADDRS</span><span class="pi">:</span> <span class="s">${DOCKER_HOST_ADDRESS}</span>
<span class="na">RTPENGINE_HOST</span><span class="pi">:</span> <span class="s">rtpengine</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">51908:51908</span>
<span class="pi">-</span> <span class="s">5060:5060</span>
<span class="pi">-</span> <span class="s">5062:5062</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">shared:/var/lib/postgresql/data</span>
<span class="na">rtpengine</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">fonoster/rtpengine:latest</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">22222:22222/udp</span>
<span class="pi">-</span> <span class="s">10000-10100:10000-10100/udp</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="na">PUBLIC_IP</span><span class="pi">:</span> <span class="s">${DOCKER_HOST_ADDRESS}</span>
<span class="na">PORT_MIN</span><span class="pi">:</span> <span class="m">10000</span>
<span class="na">PORT_MAX</span><span class="pi">:</span> <span class="m">10100</span>
<span class="na">asterisk-01</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">fonoster/mediaserver:latest</span>
<span class="na">expose</span><span class="pi">:</span>
<span class="pi">-</span> <span class="m">6060</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="na">EXTERN_ADDR</span><span class="pi">:</span> <span class="s">${DOCKER_HOST_ADDRESS}</span>
<span class="na">SIPPROXY_HOST</span><span class="pi">:</span> <span class="s">routr</span>
<span class="na">SIPPROXY_USERNAME</span><span class="pi">:</span> <span class="s">asterisk</span>
<span class="na">SIPPROXY_SECRET</span><span class="pi">:</span> <span class="s">asterisk</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">./extensions.conf:/etc/asterisk/extensions.conf</span>
<span class="pi">-</span> <span class="s">./sounds/count_1.sln16:/var/lib/asterisk/sounds/count.sln16</span>
<span class="na">asterisk-02</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">fonoster/mediaserver:latest</span>
<span class="na">expose</span><span class="pi">:</span>
<span class="pi">-</span> <span class="m">6060</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="na">EXTERN_ADDR</span><span class="pi">:</span> <span class="s">${DOCKER_HOST_ADDRESS}</span>
<span class="na">SIPPROXY_HOST</span><span class="pi">:</span> <span class="s">routr</span>
<span class="na">SIPPROXY_USERNAME</span><span class="pi">:</span> <span class="s">asterisk</span>
<span class="na">SIPPROXY_SECRET</span><span class="pi">:</span> <span class="s">asterisk</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">./extensions.conf:/etc/asterisk/extensions.conf</span>
<span class="pi">-</span> <span class="s">./sounds/count_2.sln16:/var/lib/asterisk/sounds/count.sln16</span>
<span class="na">simplephone</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">psanders/simplephone:latest</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">8080:8080</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="na">shared</span><span class="pi">:</span>
</code></pre>
</div>
<p>A few things to note:</p>
<ul>
<li>We added the shared volume to make sure the data survives container restarts</li>
<li>The simplephone and rtpengine services are optional, and you may remove them if you don't need support for WebRTC</li>
<li>Notice how we mapped the sounds count_1.sln16 to asterisk-1 and count_2.sln16 to asterisk-2 respectively.</li>
</ul>
<p>Next, in the same folder, create the file extensions.conf</p>
<p>Filename: <em>voipnet/extensions.conf</em><br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>[local-ctx]
exten => asterisk,1,NoOp()
same => n,Playback(count)
same => n,Hangup()
</code></pre>
</div>
<p>The previous configuration features the equivalent of a "Hello world" for Asterisk. However, you could use the same principle and instead connect it to a voice application or conference.</p>
<p>Then, create a sounds folder and download count_2.sln16 and count_2.sln16 from <a href="proxy.php?url=https://github.com/psanders/rtc-labs/tree/main/ha_ivr_with_routr_and_asterisk/sounds" rel="noopener noreferrer">here.</a></p>
<p>Your folder structure will end up like this:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>.
├── compose.yaml
├── extensions.conf
└── sounds
├── count_1.sln16
└── count_2.sln16
</code></pre>
</div>
<p>Finally, run the following command to start all the services:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># NOTE: Be sure to update the IP address</span>
<span class="nv">DOCKER_HOST_ADDRESS</span><span class="o">=</span>192.168.1.7 docker compose up
</code></pre>
</div>
<p>The previous command will pull all the services from Docker Hub and run the containers. You could also add the <code>-d</code> option to run the service in the background.</p>
<h2>
Configuring the network with Routr Command-Line Tool
</h2>
<p>You will use Routr's command-line tool to issue commands to the server and build the VoIP network.</p>
<p>For the setup, we will create a Domain with the <code>sip.local</code> URI, an Agent to test the calls and a Peer to represent Asterisk.</p>
<p>Remember that while we are using Agents for testing, in production, you would instead use Trunks to accept PSTN traffic, which is not covered in this tutorial.</p>
<p>We will start by creating a Domain with:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>rctl domains create <span class="nt">--insecure</span>
</code></pre>
</div>
<p>Notice the <code>--insecure</code> flag, which is required since we don't have a TLS certificate.</p>
<p>The output of your command will look similar to the output below:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>Press ^C at any <span class="nb">time </span>to quit.
› Warning: Egress rules unavailable due to 0 configured numbers.
? Friendly Name Local Domain
? SIP URI sip.local
? IP Access Control List None
? Ready? Yes
Creating Domain Local Domain... 3b20410a-3c80-4f66-b7b3-58f65ff65352
</code></pre>
</div>
<p>Next, create two sets of credentials, one for Asterisk and one for John Doe, by issuing the following command:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>rctl credentials create <span class="nt">--insecure</span>
</code></pre>
</div>
<p>Your output will be similar to this:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>This utility will <span class="nb">help </span>you create a new <span class="nb">set </span>of Credentials.
Press ^C at any <span class="nb">time </span>to quit.
? Friendly Name John Doe - Credentials
? Username 1001
? Password <span class="o">[</span>hidden]
? Ready? Yes
Creating Credentials John Doe - Credentials... 5fbc7367-a59d-4555-9fc4-a15ff29c24c8
</code></pre>
</div>
<p>Repeat the process for Asterisk, and set the password to <code>asterisk</code>.</p>
<p>Next, create an Agent to represent the test Agent, John Doe, with the following command:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>rctl agents create <span class="nt">--insecure</span>
</code></pre>
</div>
<p>Your output will be like the one below:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>This utility will <span class="nb">help </span>you create a new Agent.
Press ^C at any <span class="nb">time </span>to quit.
? Friendly Name John Doe
? Select a Domain sip.local
? Username 1001
? Credentials Name John Doe - Credentials
? Max Contacts
? Privacy None
? Enabled? Yes
? Ready? Yes
Creating Agent John Doe... 662a379d-66f1-4e6e-9df5-5126f1dcb930
</code></pre>
</div>
<p>Finally, create a Peer to represent Asterisk with:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>rctl peer create <span class="nt">--insecure</span>
</code></pre>
</div>
<p>Follow the prompt, select Round Robin as the balancing algorithm, and <code>sip:asterisk</code> as the Address of Record (AOR). </p>
<p>The output should look like this:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>This utility will <span class="nb">help </span>you create a new Peer.
Press ^C at any <span class="nb">time </span>to quit.
? Friendly Name Asterisk PBX
? Username asterisk
? Address of Record sip:asterisk
? Contact Address
? Max Contacts
? IP Access Control List None
? Credentials Name Asterisk - Credentials
? Enable Session Affinity? No
? Balancing Algorithm Round Robin
? Enabled? Yes
? Ready? Yes
Creating Peer Asterisk PBX... 1f859fee-5c4c-4771-b8fd-a6c469ffe9bf
</code></pre>
</div>
<p>You might use the <code>get</code> subcommand to verify your settings. </p>
<p>For example:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>rctl agents get <span class="nt">--insecure</span> <span class="nt">--extended</span>
</code></pre>
</div>
<h2>
Calling Asterisk from the Browser using SIP.js
</h2>
<p>The compose file we created at the beginning of this tutorial contains the simplephone container, a softphone implemented with SIP.js, which is available at <a href="proxy.php?url=http://localhost:8080" rel="noopener noreferrer">http://localhost:8080</a>.</p>
<p>Here is what the SimplePhone will look like:</p>
<p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2dm4cno1b0oa585bnpwj.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2dm4cno1b0oa585bnpwj.png" alt="Example using the SimplePhone"></a></p>
<p>Set your instance to look like the one below, then click "Save and connect," followed by "Call."</p>
<p>You should hear the numbers alternate every time you press "Call," showing that the calls are correctly load-balanced. </p>
<h2>
What’s next?
</h2>
<p>Please comment if you find this tutorial helpful and check out the following relevant tutorials:</p>
<ul>
<li><a href="proxy.php?url=https://dev.to/fonoster/calling-a-door-phone-from-a-web-browser-1bko">Calling a door phone from a web browser</a></li>
<li><a href="proxy.php?url=https://dev.to/fonoster/browser-to-browser-calling-with-sipjs-and-routr-3l07">Browser-to-Browser calling with SIP.js and Routr</a></li>
<li><a href="proxy.php?url=https://dev.to/psanders/simplify-sipjs-security-with-short-lived-tokens-mb8">Simplify SIP.js security with short-lived tokens</a></li>
</ul>
routrwebrtcasteriskvoipCalling a door phone from a web browserPedro SandersFri, 23 Feb 2024 18:27:26 +0000
https://dev.to/fonoster/calling-a-door-phone-from-a-web-browser-1bko
https://dev.to/fonoster/calling-a-door-phone-from-a-web-browser-1bko<p>Calling a door phone from a web browser</p>
<p>In this tutorial, I will take you through the steps of building a VoIP network that will allow you to make calls from a web browser out to a door phone. This setup would be helpful for access control for commercial buildings, monitoring and security, calls for assistance in hospitals, and more.</p>
<p>We will use SIP.js as the client, Routr as the signaling server, and RTPEngine to proxy RTP traffic.</p>
<div class="ltag-github-readme-tag">
<div class="readme-overview">
<h2>
<img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo">
<a href="proxy.php?url=https://github.com/fonoster" rel="noopener noreferrer">
fonoster
</a> / <a href="proxy.php?url=https://github.com/fonoster/routr" rel="noopener noreferrer">
routr
</a>
</h2>
<h3>
⚡ The future of programmable SIP servers.
</h3>
</div>
</div>
<h2>
Requirements
</h2>
<p>Before you start this tutorial, you will need the following:</p>
<ul>
<li>Docker Engine installed on your computer or the cloud</li>
<li>NodeJS 18+ (Use nvm if possible)</li>
<li>Routr command-line tool (Install with <code>npm install -g @routr/ctl</code>)</li>
<li>A door phone such as Fanvil i20S</li>
</ul>
<h2>
Running Routr and RTPEngine with Docker Compose
</h2>
<p>This tutorial will use Routr for signaling and RTPEngine to proxy RTP traffic. </p>
<p>The simplest way to run both services is using Docker Compose. </p>
<p>To run the services with Docker Compose, first, create a folder named voipnet and, in it, a file named compose.yaml with the following content:</p>
<p>Filename: <em>voipnet/compose.yml</em><br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight yaml"><code><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3"</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">routr</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">fonoster/routr-one:latest</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="na">EXTERNAL_ADDRS</span><span class="pi">:</span> <span class="s">${DOCKER_HOST_ADDRESS}</span>
<span class="na">RTPENGINE_HOST</span><span class="pi">:</span> <span class="s">rtpengine</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">51908:51908</span>
<span class="pi">-</span> <span class="s">5062:5062</span>
<span class="pi">-</span> <span class="s">5060:5060</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="c1"># Ensures the data survives container restarts</span>
<span class="pi">-</span> <span class="s">shared:/var/lib/postgresql/data</span>
<span class="c1"># RTPEngine requires network mode "host" to work properly. However, this option doesn't work on </span>
<span class="c1"># Windows and MacOs. For development, we are opening a few ports to the host machine. </span>
<span class="c1"># For production, you must use the network_mode: host which works on Linux.</span>
<span class="na">rtpengine</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">fonoster/rtpengine:latest</span>
<span class="c1"># Uncomment the following line for production</span>
<span class="c1"># network_mode: host</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="c1"># Set DOCKER_HOST_ADDRESS to an IP address that is reachable to the SIP clients</span>
<span class="na">PUBLIC_IP</span><span class="pi">:</span> <span class="s">${DOCKER_HOST_ADDRESS}</span>
<span class="na">PORT_MIN</span><span class="pi">:</span> <span class="m">10000</span>
<span class="na">PORT_MAX</span><span class="pi">:</span> <span class="m">10100</span>
<span class="na">LOG_LEVEL</span><span class="pi">:</span> <span class="m">6</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">22222:22222/udp</span>
<span class="pi">-</span> <span class="s">10000-10100:10000-10100/udp</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="na">shared</span><span class="pi">:</span>
</code></pre>
</div>
<p>Notice how we included the environment variables EXTERNAL_ADDRS and PUBLIC_IP in the previous file, whose value must be set to an IP that all parties can reach to avoid wrong signaling and audio issues.</p>
<p>Also noteworthy is that the port 51908 was opened for administration, 5060 for TCP signaling, and 5062 for signaling with SIP.js</p>
<p>Next, save the file and run the following command:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># NOTE: Be sure to update the IP address</span>
<span class="nv">DOCKER_HOST_ADDRESS</span><span class="o">=</span>192.168.1.7 docker compose up
</code></pre>
</div>
<p>The previous command will pull Routr and RTPEngine from Docker Hub and start the containers. You could also add the <code>-d</code> option to run the service in the background.</p>
<h2>
Configuring a Domain and a set of Agents
</h2>
<p>You will use Routr's command-line tool to issue commands to the server and build the VoIP network.</p>
<p>The network will have a Domain, <code>sip.local</code>, an Agent named "Door Phone," and an Agent named "Admin."</p>
<p>To build the VoIP network, first create a new Domain with:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>rctl domains create <span class="nt">--insecure</span>
</code></pre>
</div>
<p>Notice the <code>--insecure</code> flag, which is required since we don't have a TLS certificate.</p>
<p>The output of your command will look similar to the output below:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>Press ^C at any <span class="nb">time </span>to quit.
› Warning: Egress rules unavailable due to 0 configured numbers.
? Friendly Name Local Domain
? SIP URI sip.local
? IP Access Control List None
? Ready? Yes
Creating Domain Local Domain... 3b20410a-3c80-4f66-b7b3-58f65ff65352
</code></pre>
</div>
<p>Next, create two sets of credentials—one for the administrator and one for the door phone.</p>
<p>To create a set of credentials, issue the following command:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>rctl credentials create <span class="nt">--insecure</span>
</code></pre>
</div>
<p>You must do this twice, one for the Door Phone and one for Admin. Please set the Door Phone username to <code>doorphone1</code> and the Admin to <code>admin</code></p>
<p>Your output will be similar to this:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>This utility will <span class="nb">help </span>you create a new <span class="nb">set </span>of Credentials.
Press ^C at any <span class="nb">time </span>to quit.
? Friendly Name Door Phone <span class="c">#1 - Credentials</span>
? Username doorphone1
? Password <span class="o">[</span>hidden]
? Ready? Yes
Creating Credentials Door Phone - Credentials... 5fbc7367-a59d-4555-9fc4-a15ff29c24c8
</code></pre>
</div>
<p>Finally, create Agents to represent the Door Phone and the Admin using the following command:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>rctl agents create <span class="nt">--insecure</span>
</code></pre>
</div>
<p>Follow the prompt and ensure that the username matches that of the credentials.</p>
<p>Your output will look similar to this:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>This utility will <span class="nb">help </span>you create a new Agent.
Press ^C at any <span class="nb">time </span>to quit.
? Friendly Name Door Phone <span class="c">#1</span>
? Select a Domain sip.local
? Username doorphone1
? Credentials Name Door Phone <span class="c">#1 - Credentials</span>
? Max Contacts 1
? Privacy None
? Enabled? Yes
? Ready? Yes
Creating Agent Door Phone <span class="c">#1... 662a379d-66f1-4e6e-9df5-5126f1dcb930</span>
</code></pre>
</div>
<p>Be sure to repeat the process for the Admin.</p>
<p>You might use the <code>get</code> subcommand for any previously created resources to verify your settings. For example, to get a list of Agents, you might run this:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>rctl agents get <span class="nt">--insecure</span>
</code></pre>
</div>
<h2>
Configuring the door phone
</h2>
<p>The configuration on your door phone will depend on the make and model. However, generally speaking, it should have the following parameters:</p>
<ul>
<li>Username</li>
<li>Password</li>
<li>Domain</li>
<li>Proxy/Server</li>
</ul>
<p>For a Fanvil iS20, the configuration will be like this:</p>
<p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg82xwnd99x5rwghrz42w.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg82xwnd99x5rwghrz42w.png" alt="Video feed from a Fanvil unit"></a></p>
<h2>
Building a barebones tool for calling
</h2>
<p>To create the SIP Agent, we will use the SimpleAgent implementation SIP.js, which is perfect for testing.</p>
<p>For a bare-bone SIP Agent, copy the following code in a file named index.html:</p>
<p>Filename <em>voipnet/index.html</em><br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight html"><code><span class="cp"><!DOCTYPE html></span>
<span class="nt"><html</span> <span class="na">lang=</span><span class="s">"en"</span> <span class="na">xmlns=</span><span class="s">"http://www.w3.org/1999/xhtml"</span><span class="nt">></span>
<span class="nt"><head></span>
<span class="nt"><meta</span> <span class="na">charset=</span><span class="s">"utf-8"</span> <span class="nt">/></span>
<span class="nt"><title></span>Video Feed<span class="nt"></title></span>
<span class="nt"><style></span>
<span class="nt">h1</span> <span class="p">{</span> <span class="nl">font-family</span><span class="p">:</span> <span class="s2">'Courier New'</span><span class="p">,</span> <span class="n">Courier</span><span class="p">,</span> <span class="nb">monospace</span><span class="p">;</span> <span class="p">}</span>
<span class="nt"></style></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="nt"><h1></span>Door Phone Video Feed<span class="nt"></h1></span>
<span class="nt"><div</span> <span class="na">style=</span><span class="s">"border: 1px solid black; width: 300px; height: 300px;"</span><span class="nt">></span>
<span class="nt"><video</span> <span class="na">id=</span><span class="s">"remoteVideo"</span> <span class="na">autoplay</span> <span class="na">muted</span> <span class="na">style=</span><span class="s">"background-color: black; height: 100%"</span><span class="nt">></video></span>
<span class="nt"></div></span>
<span class="nt"><br</span> <span class="nt">/></span>
<span class="nt"><button</span> <span class="na">id=</span><span class="s">"callButton"</span><span class="nt">></span>Call Door Phone<span class="nt"></button></span>
<span class="nt"><button</span> <span class="na">id=</span><span class="s">"hangupButton"</span><span class="nt">></span>Hangup<span class="nt"></button></span>
<span class="nt"><audio</span> <span class="na">style=</span><span class="s">"display: none"</span> <span class="na">id=</span><span class="s">"remoteAudio"</span> <span class="na">controls</span><span class="nt">></span>
<span class="nt"><p></span>Your browser doesn't support HTML5 audio.<span class="nt"></p></span>
<span class="nt"></audio></span>
<span class="nt"><script </span><span class="na">type=</span><span class="s">"module"</span><span class="nt">></span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Web</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">https://unpkg.com/[email protected]/lib/index.js</span><span class="dl">"</span><span class="p">;</span>
<span class="nb">document</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">DOMContentLoaded</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">remoteVideo</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">remoteVideo</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">callButton</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">callButton</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">hangupButton</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">hangupButton</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">remoteAudio</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">remoteAudio</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">config</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">server</span><span class="p">:</span> <span class="dl">"</span><span class="s2">ws://localhost:5062</span><span class="dl">"</span><span class="p">,</span>
<span class="na">aor</span><span class="p">:</span> <span class="dl">"</span><span class="s2">sip:[email protected]</span><span class="dl">"</span><span class="p">,</span>
<span class="na">doorPhoneAor</span><span class="p">:</span> <span class="dl">"</span><span class="s2">sip:[email protected]</span><span class="dl">"</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">aor</span><span class="p">:</span> <span class="nx">config</span><span class="p">.</span><span class="nx">aor</span><span class="p">,</span>
<span class="na">media</span><span class="p">:</span> <span class="p">{</span>
<span class="na">constraints</span><span class="p">:</span> <span class="p">{</span> <span class="na">audio</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">video</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span>
<span class="na">remote</span><span class="p">:</span> <span class="p">{</span>
<span class="na">audio</span><span class="p">:</span> <span class="nx">remoteAudio</span><span class="p">,</span>
<span class="na">video</span><span class="p">:</span> <span class="nx">remoteVideo</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="na">userAgentOptions</span><span class="p">:</span> <span class="p">{</span>
<span class="na">authorizationUsername</span><span class="p">:</span> <span class="dl">"</span><span class="s2">admin</span><span class="dl">"</span><span class="p">,</span>
<span class="na">authorizationPassword</span><span class="p">:</span> <span class="dl">"</span><span class="s2">1234</span><span class="dl">"</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">simpleUser</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Web</span><span class="p">.</span><span class="nc">SimpleUser</span><span class="p">(</span><span class="nx">config</span><span class="p">.</span><span class="nx">server</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span>
<span class="nx">callButton</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">click</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span><span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">simpleUser</span><span class="p">.</span><span class="nf">connect</span><span class="p">();</span>
<span class="nx">simpleUser</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="nx">config</span><span class="p">.</span><span class="nx">doorPhoneAor</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">hangupButton</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">click</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">simpleUser</span><span class="p">.</span><span class="nf">hangup</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="nt"></script></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre>
</div>
<p>Then, lunch the SIP Agent with:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>npx http-server <span class="nb">.</span>
</code></pre>
</div>
<h1>
Calling the door phone from the web browser
</h1>
<p>Now that created a SIP agent using SIP.js, we only need to register the door phone to Routr and press the "Call Door Phone" button in the web browser.</p>
<p>If you have issues calling the door phone, please inspect the browser's console for errors.</p>
<p>The SIP agent should connect to the door phone, and you should see a video feed.</p>
<p>Example of the output:</p>
<p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74qjqlh0agkjmimfjckn.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74qjqlh0agkjmimfjckn.png" alt="Video feed from a Fanvil unit"></a></p>
<h2>
What's next?
</h2>
<p>Please comment if you find this tutorial helpful and check out the following relevant tutorials:</p>
<ul>
<li><a href="proxy.php?url=https://dev.to/fonoster/building-scalable-ivrs-for-businesses-with-routr-and-asterisk-cjd">Building scalable IVRs for businesses with Routr and Asterisk</a></li>
<li><a href="proxy.php?url=https://dev.to/fonoster/browser-to-browser-calling-with-sipjs-and-routr-3l07">Browser-to-Browser calling with SIP.js and Routr</a></li>
<li><a href="proxy.php?url=https://dev.to/psanders/simplify-sipjs-security-with-short-lived-tokens-mb8">Simplify SIP.js security with short-lived tokens</a></li>
</ul>
javascriptroutrsipwebrtcBrowser-to-Browser calling with SIP.js and RoutrPedro SandersThu, 22 Feb 2024 13:08:27 +0000
https://dev.to/fonoster/browser-to-browser-calling-with-sipjs-and-routr-3l07
https://dev.to/fonoster/browser-to-browser-calling-with-sipjs-and-routr-3l07<p>In this tutorial, I will show you how to use SIP.js and Routr to develop seamless calling experiences without losing your hair. By the end of this tutorial, you will be able to apply the same principles to building 1-1 video calls, chat applications, click-to-call buttons, and more.</p>
<p>Remember that this is one way to accomplish this task, and it is especially relevant if you plan to make future calls to the PSTN (Private Switch Telephone Network).</p>
<div class="ltag-github-readme-tag">
<div class="readme-overview">
<h2>
<img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo">
<a href="proxy.php?url=https://github.com/fonoster" rel="noopener noreferrer">
fonoster
</a> / <a href="proxy.php?url=https://github.com/fonoster/routr" rel="noopener noreferrer">
routr
</a>
</h2>
<h3>
⚡ The future of programmable SIP servers.
</h3>
</div>
</div>
<h2>
Requirements
</h2>
<p>Before you start this tutorial, you will need the following:</p>
<ul>
<li>Docker Engine installed on your computer or the cloud</li>
<li>NodeJS 18+ (Use nvm if possible)</li>
<li>Routr command-line tool (Install with <code>npm install -g @routr/ctl</code>)</li>
</ul>
<h2>
Running Routr server with Docker Compose
</h2>
<p>This tutorial will use Routr to establish a call between two phones running on separate browsers.</p>
<p>The simplest way to run Routr is using Docker Compose. </p>
<p>To run Routr with Docker Compose, first, create a folder named voipnet and in it, a file named compose.yaml with the following content:</p>
<p>Filename: <em>voipnet/compose.yml</em></p>
<div class="highlight js-code-highlight">
<pre class="highlight yaml"><code>
<span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3"</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">routr</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">fonoster/routr-one:latest</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="na">EXTERNAL_ADDRS</span><span class="pi">:</span> <span class="s">${DOCKER_HOST_ADDRESS}</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">51908:51908</span>
<span class="pi">-</span> <span class="s">5062:5062</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="c1"># Ensures the data survives container restarts</span>
<span class="pi">-</span> <span class="s">shared:/var/lib/postgresql/data</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="na">shared</span><span class="pi">:</span>
</code></pre>
</div>
<p>Notice how we included the environment variable EXTERNAL_ADDRS in the previous file, whose value must be set to an IP that all parties can reach to avoid wrong signaling.</p>
<p>Also noteworthy is that the ports 51908 and 5062 were opened for administration and signaling.</p>
<p>Next, save the file and run the following command:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
<span class="c"># NOTE: Be sure to update the IP address</span>
<span class="nv">DOCKER_HOST_ADDRESS</span><span class="o">=</span>192.168.1.7 docker compose up
</code></pre>
</div>
<p>The previous command will pull Routr from Docker Hub and run the container. You could also add the <code>-d</code> option to run the service in the background.</p>
<h2>
Configuring the VoIP network
</h2>
<p>You will use Routr's command-line tool to issue commands to the server and build the VoIP network.</p>
<p>To build the VoIP network, first create a new Domain with:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
rctl domains create <span class="nt">--insecure</span>
</code></pre>
</div>
<p>Notice the <code>--insecure</code> flag, which is required since we don't have a TLS certificate.</p>
<p>The output of your command will look similar to the output below:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
Press ^C at any <span class="nb">time </span>to quit.
› Warning: Egress rules unavailable due to 0 configured numbers.
? Friendly Name Local Domain
? SIP URI sip.local
? IP Access Control List None
? Ready? Yes
Creating Domain Local Domain... 3b20410a-3c80-4f66-b7b3-58f65ff65352
</code></pre>
</div>
<p>Next, create two sets of credentials—one for John and one for Jane.</p>
<p>To create a set of credentials, issue the following command:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
rctl credentials create <span class="nt">--insecure</span>
</code></pre>
</div>
<p>You must do this twice, one for John and one for Jane. Please set John's username to 1001 and Jane's to 1002.</p>
<p>Your output will be similar to this:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
This utility will <span class="nb">help </span>you create a new <span class="nb">set </span>of Credentials.
Press ^C at any <span class="nb">time </span>to quit.
? Friendly Name John Doe - Credentials
? Username 1001
? Password <span class="o">[</span>hidden]
? Ready? Yes
Creating Credentials John Doe - Credentials... 5fbc7367-a59d-4555-9fc4-a15ff29c24c8
</code></pre>
</div>
<p>Finally, create Agents to represent John and Jane using the following command:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
rctl agents create <span class="nt">--insecure</span>
</code></pre>
</div>
<p>Follow the prompt and ensure that the username matches that of the credentials.</p>
<p>Your output will look similar to this:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
This utility will <span class="nb">help </span>you create a new Agent.
Press ^C at any <span class="nb">time </span>to quit.
? Friendly Name John Doe
? Select a Domain sip.local
? Username 1001
? Credentials Name John Doe - Credentials
? Max Contacts 1
? Privacy None
? Enabled? Yes
? Ready? Yes
Creating Agent John Doe... 662a379d-66f1-4e6e-9df5-5126f1dcb930
</code></pre>
</div>
<p>Be sure to repeat the process for Jane.</p>
<p>You might use the <code>get</code> subcommand for any previously created resources to verify your settings. For example, to get a list of Agents, you might run this:</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>
rctl agents get <span class="nt">--insecure</span>
</code></pre>
</div>
<h2>
Running the SimplePhone with Docker Compose
</h2>
<p>SimplePhone is a phone built using SIP.js that runs as a Docker container. Please see the documentation at <a href="proxy.php?url=https://sipjs.com/" rel="noopener noreferrer">https://sipjs.com/</a> to develop your implementation.</p>
<p>To run the phone, you must first update the compose.yaml file with the following code:</p>
<p>Filename: <em>voipnet/compose.yaml</em></p>
<div class="highlight js-code-highlight">
<pre class="highlight yaml"><code>
<span class="s">--snip--</span>
<span class="s">simplephone</span><span class="err">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">psanders/simplephone:latest</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="na">NODE_ENV</span><span class="pi">:</span> <span class="s">production</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">8080:8080</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="na">shared</span><span class="pi">:</span>
</code></pre>
</div>
<p>Then, re-run the <code>docker compose up</code> command. You can go to <a href="proxy.php?url=http://localhost:8080" rel="noopener noreferrer">http://localhost:8080</a> to see the phone when all the services are up.</p>
<h2>
Making a call between the two phones
</h2>
<p>Remember the Agents you created at the start of this tutorial? You will now use the same values to configure two phone instances. </p>
<p>On a tab on your browser, open an instance of the SimplePhone and enter the information for John; click "Save and connect" followed by "Register."</p>
<p>The SimplePhone will look similar to this:</p>
<p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1dgs1h5asw41n7hpxj5e.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1dgs1h5asw41n7hpxj5e.png" alt="A screenshot of the SimplePhone filled with John Doe's credentials"></a></p>
<p>If you have issues connecting or registering, please inspect the browser's console for errors.</p>
<p>Then, open a new tab or browser and repeat the process for Jane.</p>
<p>Finally, click on the "Call" button to reach John or Jane.</p>
<h2>
What's next?
</h2>
<p>Please comment if you find this tutorial helpful and check out the following relevant tutorials:</p>
<ul>
<li><a href="proxy.php?url=https://dev.to/fonoster/calling-a-door-phone-from-a-web-browser-1bko">Calling a door phone from a web browser</a></li>
<li><a href="proxy.php?url=https://dev.to/fonoster/building-scalable-ivrs-for-businesses-with-routr-and-asterisk-cjd">Building scalable IVRs for businesses with Routr and Asterisk</a></li>
<li><a href="proxy.php?url=https://dev.to/psanders/simplify-sipjs-security-with-short-lived-tokens-mb8">Simplify SIP.js security with short-lived tokens</a></li>
</ul>
voipsipjavascriptwebrtcThe essentials of building Voice Applications with FonosterPedro SandersSat, 03 Jul 2021 13:24:00 +0000
https://dev.to/fonoster/the-essentials-of-building-voice-applications-with-project-fonos-32g2
https://dev.to/fonoster/the-essentials-of-building-voice-applications-with-project-fonos-32g2<p>The purpose of this tutorial is to show the basics of Fonoster. Here you will find how to create a Voice Application, add a Number, and then use that Number to originate a call. Please follow the guide in sequence, as each step builds on the last one.</p>
<div class="ltag-github-readme-tag">
<div class="readme-overview">
<h2>
<img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo">
<a href="proxy.php?url=https://github.com/fonoster" rel="noopener noreferrer">
fonoster
</a> / <a href="proxy.php?url=https://github.com/fonoster/fonoster" rel="noopener noreferrer">
fonoster
</a>
</h2>
<h3>
🚀 The open-source alternative to Twilio.
</h3>
</div>
</div>
<h2>
Requirements
</h2>
<p>Before you start this guide, you will need the following:</p>
<ul>
<li>A set of credentials from <a href="proxy.php?url=https://console.fonoster.io" rel="noopener noreferrer">here</a> 👈</li>
<li>An account for access to a SIP Service Provider (For US and Canada, we recommend <a href="proxy.php?url=https://voip.ms/en/invite/MjE1NzA2" rel="noopener noreferrer">voip.ms</a>)</li>
<li>NodeJS 14+ (Use nvm if possible)</li>
<li>Fonoster command-line tool (install with <code>npm install -g @fonoster/ctl</code>)</li>
<li>Ngrok (install with <code>npm install -g ngrok</code>)</li>
</ul>
<p>You can login to the server with:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>fonoster auth:login
</code></pre>
</div>
<p>And your output will be similar to:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>Access your Fonoster infrastructure
Press ^C at any <span class="nb">time </span>to quit.
? api endpoint api.fonoster.io
? access key <span class="nb">id </span>psanders
? access key token <span class="k">*************************</span>...
? ready? Yes
Accessing endpoint api.fonoster.io... Done
</code></pre>
</div>
<p>Then, set the default Project:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># Get the PROJECT_ID of the project using the 'projects:list' command </span>
fonoster projects:use <span class="k">${</span><span class="nv">PROJECT_ID</span><span class="k">}</span>
</code></pre>
</div>
<h2>
Creating a basic Voice Application
</h2>
<p>A Voice Application is a server that takes control of the flow of a call. A Voice Application can use any combination of the following verbs:</p>
<ul>
<li>
<code>Answer</code> - Accepts the call</li>
<li>
<code>Hangup</code> - Closes the call</li>
<li>
<code>Play</code> - It takes an URL or file and streams the sound back to the calling party</li>
<li>
<code>Say</code> - It takes a text, synthesizes the text into audio, and streams the result</li>
<li>
<code>Gather</code> - It waits for DTMF events and returns back the result</li>
<li>
<code>SGather</code> - It listen for a stream DTMF events and returns back the result</li>
<li>
<code>Record</code> - It records the voice of the calling party and saves the audio on the Storage sub-system</li>
<li>
<code>Mute</code> - It tells the channel to stop sending media, thus effectively muting the channel</li>
<li>
<code>Unmute</code> - It tells the channel to allow media flow</li>
</ul>
<p>To create a Voice Application perform the following steps.</p>
<p>First, clone the NodeJS example template as follows:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>git clone https://github.com/fonoster/nodejs-voiceapp
</code></pre>
</div>
<p>Next, install the dependencies:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>cd nodejs-voiceapp
npm install
</code></pre>
</div>
<p>Finally, launch the Voice Application with:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>npm start
</code></pre>
</div>
<p>Your output will look like this:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>info: initializing voice server
info: starting voice server on @ 0.0.0.0, port=3000
</code></pre>
</div>
<blockquote>
<p>Your app will live at <code>http://127.0.0.1:3000</code>. ⚠️ Be sure to leave the server up!</p>
</blockquote>
<h2>
Using Ngrok to publish your Voice Application
</h2>
<p>Now that we have our Voice Application up and running, we need to make it available on the Internet——the fastest way to enable public access by using Ngrok. For example, with ngrok, you can publish a web server with a single command.</p>
<p>On a new console, run Ngrok with the following command:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>ngrok http 3000
</code></pre>
</div>
<p>The output will look like this:</p>
<p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Ffonoster%2Fblog%2Fmain%2F2021%2F002%2Fngrok_output.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Ffonoster%2Fblog%2Fmain%2F2021%2F002%2Fngrok_output.png" alt="Ngrok output"></a></p>
<p>Leave this service running, and save the <code>Forwarding URL</code> for use in the next step.</p>
<h2>
Building a SIP Network
</h2>
<p>A SIP Network has all the building blocks needed to establish communication between two SIP endpoints(i.e., softphone, webphone, cellphone, the PSTN, etc.) We want to configure a Number and route the calls to our Voice Application on this guide.</p>
<p>Let's start by creating a SIP Service Provider.</p>
<h3>
Adding a SIP Service Provider
</h3>
<p>A SIP Service Provider is an organization that will terminate your calls to the phone network (or PSTN). You will need the <code>username</code>, <code>password</code>, and <code>host</code> you obtained from your SIP Service Provider for this section.</p>
<p>Create a new provider with:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>fonoster providers:create
</code></pre>
</div>
<p>The output will look similar to this:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>This utility will help you create a new Provider
Press ^C at any time to quit.
? friendly name VOIPMS
? username 215706
? secret [hidden]
? host newyork1.voip.ms
? transport tcp
? expire 300
? ready? Yes
Creating provider YourServiceProvider... Done
</code></pre>
</div>
<h3>
Adding a SIP Number
</h3>
<p>A Number, often referred to as DID/DOD, is a number managed by your SIP Service provider. </p>
<blockquote>
<p>If your Provider doesn't accept E164, you can append the <code>--ignore-e164-validation</code><br>
</p>
</blockquote>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>fonoster numbers:create <span class="nt">--ignore-e164-validation</span>
</code></pre>
</div>
<p>Here is an example of the output:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>This utility will <span class="nb">help </span>you create a new Number
Press ^C at any <span class="nb">time </span>to quit.
? number <span class="k">in </span>E.164 format <span class="o">(</span>e.g. +16471234567<span class="o">)</span> 9842753574
? service provider VOIPMS
? aor <span class="nb">link</span> <span class="o">(</span>leave empty<span class="o">)</span>
? webhook https://5a2d2ea5d84d.ngrok.io <span class="c"># Replace with the value you obtained from Ngrok</span>
? ready? Yes
Creating number +17853178071... KyjgGEkasj
</code></pre>
</div>
<blockquote>
<p>⚠️ Be sure to replace the information with what was given to you by your Provider.</p>
</blockquote>
<h3>
Creating a SIP Domain
</h3>
<p>A SIP Domain is a space within the SIP Network where SIP entities live (usually SIP Agents). To create a SIP Domain, you can use the command-line tool or the SDK.</p>
<p>In this step, you need to select the Number you just created as your <code>Egreess Number</code>. Also, make sure to use an "unclaimed" <code>uri</code> or you will receive this error: "› Error: This Domain already exists." </p>
<p>Create a new Domain with:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>fonoster domains:create
</code></pre>
</div>
<p>Your output will look similar to this:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>This utility will <span class="nb">help </span>you create a new Domain
Press ^C at any <span class="nb">time </span>to quit.
? friendly name Acme Corp
? domain uri <span class="o">(</span>e.g acme.com<span class="o">)</span> sip.acme.com
? egress number none
? egress rule .<span class="k">*</span>
? ready? Yes
Creating domain Acme Corp... Jny9B_qaIh
</code></pre>
</div>
<blockquote>
<p>⚠️ In the demo server, you don't need to own the Domain. Any available URI is fair game!</p>
</blockquote>
<h2>
Using the API to make a call
</h2>
<p>To make a call, you need install the SDK.</p>
<p>Install the SDK, from within the <code>voiceapp</code>, with:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>npm <span class="nb">install</span> <span class="nt">--save</span> @fonoster/sdk
</code></pre>
</div>
<p>Next, create the script <code>call.js</code> with the following code:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="c1">// This will load the SDK and reuse your Fonoster credentials</span>
<span class="kd">const</span> <span class="nx">Fonoster</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">@fonoster/sdk</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">callManager</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Fonoster</span><span class="p">.</span><span class="nc">CallManager</span><span class="p">();</span>
<span class="c1">// Few notes:</span>
<span class="c1">// 1. Update the from to look exactly as the Number you added </span>
<span class="c1">// 2. Use an active phone or mobile</span>
<span class="c1">// 3. Replace the webhook with the one from your Ngrok</span>
<span class="nx">callManager</span><span class="p">.</span><span class="nf">call</span><span class="p">({</span>
<span class="na">from</span><span class="p">:</span> <span class="dl">"</span><span class="s2">9842753574</span><span class="dl">"</span><span class="p">,</span>
<span class="na">to</span><span class="p">:</span> <span class="dl">"</span><span class="s2">17853178070</span><span class="dl">"</span><span class="p">,</span>
<span class="na">webhook</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://5a2d2ea5d84d.ngrok.io</span><span class="dl">"</span><span class="p">,</span>
<span class="na">ignoreE164Validation</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">})</span>
<span class="p">.</span><span class="nf">then</span><span class="p">(</span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">)</span>
<span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">);</span>
</code></pre>
</div>
<p>Finally, run your script with: <code>node call.js</code></p>
<p>If everything goes well, you will start seeing the output in the console you are running your Voice Application. You will also receive a call that will stream a "Hello World," which further confirms that everything is behaving as it should.</p>
<p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Ffonoster%2Fblog%2Fmain%2F2021%2F002%2Fcall_request.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Ffonoster%2Fblog%2Fmain%2F2021%2F002%2Fcall_request.png" alt="Call Request output"></a></p>
<h2>
Troubleshooting
</h2>
<h3>
1. Are you not receiving the call at all?
</h3>
<p>The first thing to check is that your SIP Service Provider configuration is correct. Next, double-check the <code>username</code>, <code>password</code>, and <code>host</code>. If your Provider has an Admin console, check if you can see the registration from Fonoster.</p>
<p>Next, make sure the <code>from</code> matches the Number given to you by your Provider. Also, double-check the <code>to</code> has the correct prefix (for example, +1, etc.).</p>
<h3>
2. You receive the call but immediately hang up (did not hear a sound)
</h3>
<p>First, verify that Ngrok is still running. Next, compare Ngrok's URL with the webhook on your Number. They both need to match!</p>
<p>Then observe the console's output where your Voice Application is running, and see if there are any errors.</p>
<h2>
Giving feedback to Team Fonoster
</h2>
<p>We want to provide you with the best possible experience. To do that, we need your valuable feedback. Because we know you are busy, we provide two ways to get quick feedback from you. From the command line, use the <code>fonoster bug</code> command to start a GitHub issue. Or, you can use the <code>fonoster feedback</code> command to complete a short survey (which takes less than 30 seconds).</p>
<div class="ltag-github-readme-tag">
<div class="readme-overview">
<h2>
<img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo">
<a href="proxy.php?url=https://github.com/fonoster" rel="noopener noreferrer">
fonoster
</a> / <a href="proxy.php?url=https://github.com/fonoster/fonoster" rel="noopener noreferrer">
fonoster
</a>
</h2>
<h3>
🚀 The open-source alternative to Twilio.
</h3>
</div>
</div>
fonostertwilionodejavascriptHow we created an open-source alternative to Twilio and why it mattersPedro SandersFri, 02 Jul 2021 10:55:00 +0000
https://dev.to/fonoster/how-we-created-an-open-source-alternative-to-twilio-and-why-it-matters-434g
https://dev.to/fonoster/how-we-created-an-open-source-alternative-to-twilio-and-why-it-matters-434g<p>Last year, when I started assembling Team Fonoster, I published a <a href="proxy.php?url=https://www.reddit.com/r/Entrepreneur/comments/j96avf/an_opensource_alternative_to_twilio/" rel="noopener noreferrer">post</a> on Reddit that sparked a great conversation and placed Fonoster on Github's trending list even though we didn't have much to show.</p>
<div class="ltag-github-readme-tag">
<div class="readme-overview">
<h2>
<img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo">
<a href="proxy.php?url=https://github.com/fonoster" rel="noopener noreferrer">
fonoster
</a> / <a href="proxy.php?url=https://github.com/fonoster/fonoster" rel="noopener noreferrer">
fonoster
</a>
</h2>
<h3>
🚀 The open-source alternative to Twilio.
</h3>
</div>
</div>
<p>As a result, I had the opportunity to interview dozens of CTOs from companies worldwide and speak with several investors who were interested in the idea of an open-source stack of Programmable Telecommunications.</p>
<p>In the interviews, I found we need an innovative approach to a cloud-based stack for Programmable Telecommunications.</p>
<h2>
Why we needed CPaaS in the first place?
</h2>
<p>Building an application that takes advantage of the existing Telecom network has always been a difficult task compared with, for example, building a web-based application.</p>
<p>This is difficult because it involves a particular set of skills that is challenging to find and can get really costly.</p>
<p>Let's face it, no one wants to read through dozens of RFCs to program a phone call.</p>
<p>So, when the API era arrived along with UCaaS and CPaaS providers, it was a no-brainer to use one of those providers to deploy a solution within weeks instead of spending months only to get a simple use-case.</p>
<h2>
So what's wrong with traditional CPaaS?
</h2>
<p>There is nothing wrong with traditional CPaaS. In fact, in most cases, using a CPaaS is a great option to deploy a Telecommunications solution.</p>
<p>However, even though the concept of using a CPaaS to go to market quickly is fantastic, it comes at a high price for some use-cases. After all, if something goes wrong, you will have no other option but to migrate to another CPaaS or build your own solution and start again on square zero.</p>
<p>Some companies complain about the high prices for using a CPaaS. A startup CTO once told me, “It almost feels that we are paying for a lot of features we don't need.” This is because, with a traditional CPaaS, you start on a pay-as-you-go model, but costs can quickly get out of control.</p>
<p>Other companies find themselves limited by their providers' features because with traditional CPaaS you have no option but to use what they have available. There is no chance for customization. And even though that's not a problem for most companies, it is a deal-breaker for technology companies.</p>
<p>Then you have use-cases, especially in the healthcare industry, that can't benefit from using a traditional CPaaS due to privacy concerns and local regulations.</p>
<p>In which of those categories does your company fall?</p>
<h2>
How can we make this better?
</h2>
<p>The primary innovation of Fonoster lies in researching and developing the means for creating a highly portable, cloud-based Programmable Telecommunications stack.</p>
<p>This Programmable Telecommunications stack will allow businesses to call an API to dial, answer a call, establish a video session, send SMS, etc. There won't be any concern about what servers and networks are doing with that information in the background.</p>
<p>Our overall approach to building Fonoster is to use existing open-source solutions that are best in their class when possible and build our own when necessary. We then integrate this individual open-source software into a cohesive set of APIs that resembles a traditional CPaaS.</p>
<p>For example, to start a simple Voice Application one could write a Javascript code like the one below:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="kd">const</span> <span class="p">{</span> <span class="nx">VoiceServer</span> <span class="p">}</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">@fonoster/voice</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">serverConfig</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">pathToFiles</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">process</span><span class="p">.</span><span class="nf">cwd</span><span class="p">()}</span><span class="s2">/sounds`</span><span class="p">,</span>
<span class="p">};</span>
<span class="k">new</span> <span class="nc">VoiceServer</span><span class="p">(</span><span class="nx">serverConfig</span><span class="p">).</span><span class="nf">listen</span><span class="p">(</span>
<span class="k">async </span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">req</span><span class="p">);</span>
<span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nf">answer</span><span class="p">();</span>
<span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nf">play</span><span class="p">(</span><span class="s2">`sound:</span><span class="p">${</span><span class="nx">req</span><span class="p">.</span><span class="nx">selfEndpoint</span><span class="p">}</span><span class="s2">/sounds/hello-world.sln16`</span><span class="p">);</span>
<span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nf">hangup</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">);</span>
</code></pre>
</div>
<p>Or to make a call to the telephone network, you could use the SDK and write a simple script like this:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">Fonoster</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">@fonoster/sdk</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">callManager</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Fonoster</span><span class="p">.</span><span class="nc">CallManager</span><span class="p">();</span>
<span class="nx">callManager</span><span class="p">.</span><span class="nf">call</span><span class="p">({</span>
<span class="na">from</span><span class="p">:</span> <span class="dl">"</span><span class="s2">9842753574</span><span class="dl">"</span><span class="p">,</span>
<span class="na">to</span><span class="p">:</span> <span class="dl">"</span><span class="s2">17853178070</span><span class="dl">"</span><span class="p">,</span>
<span class="na">webhook</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://5a2d2ea5d84d.ngrok.io</span><span class="dl">"</span>
<span class="p">})</span>
<span class="p">.</span><span class="nf">then</span><span class="p">(</span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">)</span>
<span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">);</span>
</code></pre>
</div>
<p>Want to create a reminders application? No problem, in few easy steps, you can create and deploy a Cloud Function that will run based on a given Cron schedule.</p>
<p>First, initialize your Cloud Function with:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>fonoster funcs:init
</code></pre>
</div>
<p>Then, edit the handler with the following code:<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">Fonoster</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">@fonoster/sdk</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">callManager</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Fonoster</span><span class="p">.</span><span class="nc">CallManager</span><span class="p">();</span>
<span class="c1">// 🚀 Let's get started</span>
<span class="c1">// Use fonoster funcs:deploy to send to the cloud functions</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="k">async</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">response</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">callManager</span><span class="p">.</span><span class="nf">call</span><span class="p">({</span>
<span class="na">from</span><span class="p">:</span> <span class="dl">"</span><span class="s2">9842753589</span><span class="dl">"</span><span class="p">,</span>
<span class="na">to</span><span class="p">:</span> <span class="dl">"</span><span class="s2">17853178070</span><span class="dl">"</span><span class="p">,</span>
<span class="na">webhook</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://5a2d2ea5d84d.ngrok.io</span><span class="dl">"</span>
<span class="p">})</span>
<span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nf">succeed</span><span class="p">(</span><span class="dl">"</span><span class="s2">OK</span><span class="dl">"</span><span class="p">);</span>
<span class="p">};</span>
</code></pre>
</div>
<p>Finally, deploy to the Cloud Functions subsystem with a Cron string.<br>
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>fonoster funcs:deploy <span class="nt">--schedule</span> <span class="s2">"*/5 * * * *"</span>
</code></pre>
</div>
<p>You get the idea. </p>
<blockquote>
<p>The Cloud Functions capability if offered by the integration with OpenFaaS (by Alex Ellis)</p>
</blockquote>
<h2>
What's next?
</h2>
<p>Be sure to check <a href="proxy.php?url=https://github.com/fonoster/blog/blob/main/2021/002/post.md" rel="noopener noreferrer">The essentials of building Voice Applications with Fonoster</a> to overview the Programmable Voice features available on Project Fonoster. Star the project on <a href="proxy.php?url=https://github.com/fonoster/fonoster" rel="noopener noreferrer">Github</a> and contact us via:</p>
<ul>
<li>Twitter: @fonoster</li>
<li>Email: <a href="proxy.php?url=mailto:[email protected]">[email protected]</a>
</li>
<li><a href="proxy.php?url=https://form.typeform.com/to/CvQqk9" rel="noopener noreferrer">Slack channel</a></li>
</ul>
<div class="ltag-github-readme-tag">
<div class="readme-overview">
<h2>
<img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo">
<a href="proxy.php?url=https://github.com/fonoster" rel="noopener noreferrer">
fonoster
</a> / <a href="proxy.php?url=https://github.com/fonoster/fonoster" rel="noopener noreferrer">
fonoster
</a>
</h2>
<h3>
🚀 The open-source alternative to Twilio.
</h3>
</div>
</div>
javascriptfonosternode