Receive Realtime Socket Push in Pure Bash
Tags: node, javascript, linux, bash
Published: 2021-12-15 19:30:47

Cover Photo Credit -- [Robert Katzki](https://unsplash.com/@ro_ka) / [Unsplash](https://unsplash.com/)

> **TL;DR**: Use [`read`](https://linuxcommand.org/lc3_man_pages/readh.html) and [net-redirections in bash](https://www.xmodulo.com/tcp-udp-socket-bash-shell.html) in a few nested infinite loops to handle pushes form a server in real time and use [notify-send](https://manpages.ubuntu.com/manpages/xenial/man1/notify-send.1.html) to display a notification.

Today we're gonna receive realtime push notifications to our Ubuntu Desktops with nothing but [Bash](https://en.wikipedia.org/wiki/Bash_(Unix_shell)).

We obviously need a server to send the push notifications. We'll implement that in [Node](https://nodejs.org/en/) and [Express](https://expressjs.com/) for now, and maybe recreate that in bash too in a future article *(no guarantees though)*.

Please note that yes, we can do this really easily with polling but this post is actually about [net-redirections in Bash](https://www.xmodulo.com/tcp-udp-socket-bash-shell.html) and how it allows for true push, without the need for [(long) polling](https://ably.com/topic/long-polling) a server with something like [curl](https://curl.se/).

Are you ready? 'cause I am.

![via GIPHY](https://media.giphy.com/media/CjmvTCZf2U3p09Cn0h/giphy.gif)

# First, The Server

We kept it simple, just a http server, that receives **JSON** data as **POST** on port `9091` and sends it over TCP bidirectional sockets that are registered through a TCP server running on port `9090`. In the following format ...

```
[notification title]
[notification description]
```

The title and description on lines separated by a `\n` , that means every odd line from the server will be a title and every even numbered line will be a description.

Install [Express](https://expressjs.com/) first, then we can talk.

```bash
    yarn add express
```

For receiving the JSON payload, we are using express because personally I just like that express comes built-in with a [JSON body parser](https://expressjs.com/en/api.html#express.json) now. On the TCP end, we are using node's built-in [`net` package](https://nodejs.org/api/net.html), and storing each socket object's reference temporarily in a [JavaScript Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set).

```js
const net = require("net");
const express = require("express");

const sockets = new Set();
const app = express().use(express.json());

app.post("/", (req, res) => {
    sockets.forEach((socket) => {
        socket.write(`${req.body.title}\n${req.body.description}\n`);
    });

    res.json({
        ...req.body,
        sentTo: sockets.size,
    });
});

const server = net.createServer((socket) => {
    sockets.add(socket);

    socket.on("close", () => {
        sockets.delete(socket);
    });
});

server.listen(9090, "0.0.0.0", () => {
    console.log(`Listen on 0.0.0.0:9090`);
});

app.listen(9091, "0.0.0.0", () => {
    console.log(`Server on http://0.0.0.0:9091`);
});
```

That's it, that's the server, nothing fancy. No authentication either. I'm sure the great people before me have covered it in detail.

![Via Tenor](https://c.tenor.com/kC5DJ75S8HsAAAAd/server-for-server.gif)

# Bash = The Fun Part

## Requirements

Bash (no, *really*, it **has** to be BASH - Bourne Again Shell).

Also, bash needs to be compiled with `--enable-net-redirections`.

## Why Not Other Shells?

If you're ever working with UNIX or UNIX-like operating systems like Linux or MacOS, whenever you communicate with literally anything in the real world, you do it through a [file descriptor](https://www.computerhope.com/jargon/f/file-descriptor.htm), because often file descriptors "describe" a file on your computer or device connected to your computer.

Bash actually takes it a step further, and allows you to open TCP or UDP ports to remote servers as file descriptors, such that by writing and reading from one such file descriptor, you will be communicating with a remote server.

> There may be other shell programs that also allow this, but I am not adventurous enough to get to know them.
> – OP

## Open a File Descriptor

![via Giphy](https://media.giphy.com/media/kuIe7zXbAmeuk/giphy.gif)

First, we need to know how to open file descriptors to remote TCP ports.

```bash
exec 7<> /dev/tcp/localhost/9090
```

Hey, that was easy, we should do this more often.
- The `7` is an index for the file descriptor, so that we can refer to it by number later.
- The `<>` signifies this is a read-write descriptor, the write isn't useful for our use case but meh, shouldn't hurt.
- You can replace `/dev/tcp/` with `/dev/udp/` to do UDP communication if you want.
- `localhost` and `9090` are host and port respectively.

## Read From File Descriptor

![Via Tenor](https://c.tenor.com/Lj5SFh_tVzkAAAAC/books-read.gif)

So there's a [`read` command](https://linuxcommand.org/lc3_man_pages/readh.html).

```bash
read -u 7 TITLE
```

*How convenient*.
You might be wondering ...

> Don't I need to read the data and put it into a variable?
> – You

Yes, yes we do and that's exactly what the `TITLE` thing is. `read` reads from the file descriptor mentioned in the `-u` parameter (in our case, `7`) and puts it in a variable named at the first ordered argument (in our case `TITLE`)

It's also important to note that `read` reads upto and including a `\n` (new-line character), and blocks until it reads a new-line character into buffer or until the file descriptor closes.

If you want to prove it, you can `echo` it.

```bash
exec 7<> /dev/tcp/localhost/9090
read -u 7 TITLE
echo $TITLE
```

Note: This is assuming the server running at **9090** is writing something to the stream upon connecting, which isn't true in the code above. This is just for illustrative purposes.
Read the [`read` man pages](https://linuxcommand.org/lc3_man_pages/readh.html) for a full list of flags and arguments.

### How Does `read` Introduce Variables?

![Wait, that's illegal](https://omranjamal.me/content/images/2021/12/image.png)

Well, surprise. `read` isn't an external program. Neither is `exec`. They are both [provided by bash](https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html) (or any other shell you are currently using) to make your (the programmer) life easier.

They are kinda introduced in the same way `/dev/tcp` was. A virtual program.

## Do It Twice & Show Notification

We'll use `[notify-send](https://manpages.ubuntu.com/manpages/xenial/man1/notify-send.1.html)`

```bash
exec 7<> /dev/tcp/localhost/9090

read -u 7 TITLE
read -u 7 DESCRIPTION

notify-send "$TITLE" "$DESCRIPTION"
```

It should show you something like this on your screen *(if you're using Unity Desktop like me)*
![Ubuntu - Unity Desktop notification bubble.](https://omranjamal.me/content/images/2021/12/image-1.png)Unity Desktop Notification Bubble
That's once down. Need to do it forever.

## Do It Forever

![Via Tenor](https://c.tenor.com/IkAda59JrXEAAAAC/loop.gif)

Infinite loop time.

```bash
exec 7<> /dev/tcp/localhost/9090

while :
do
    read -u 7 TITLE
    read -u 7 DESCRIPTION

    notify-send "$TITLE" "$DESCRIPTION"
done
```

TBH, this should suffice.

## But, What If The Connection Drops?

![Via Tenor](https://c.tenor.com/KEzW7ALwfUAAAAAC/cat-what.gif)

One more infinite loop to go.

```bash
while :
do
    # Make sure the connection has been established.
    if exec 7<> /dev/tcp/localhost/9090 ; then
        while :
        do
            # Make sure, both title and description has been read.
            if read -u 7 TITLE && read -u 7 DESCRIPTION ; then
                notify-send "$TITLE" "$DESCRIPTION"
            else
                # `read` failing means there's something wrong
                # with the file descriptor (may be closed.)
                break
            fi
        done
    fi
    
    # Sleep 5 seconds, before retrying.
    sleep 5
done
```

This is might be a bit much to unpack, but read the comments.

## [Hardcoding Is Horrible](https://stackoverflow.com/questions/828207/what-is-your-attitude-towards-hard-coding)

Thankfully, shells allow you to [use and pass arguments](https://www.baeldung.com/linux/use-command-line-arguments-in-bash-script), so that we don't have to hardcode the host and port.

```bash
while :
do
    if exec 7<> /dev/tcp/$1/$2 ; then
        while :
        do
            if read -u 7 TITLE && read -u 7 DESCRIPTION ; then
                notify-send "$TITLE" "$DESCRIPTION"
            else
                break
            fi
        done
    fi
    
    sleep 5
done
```

Now you can run it like this ...

```bash
bash ./client.sh localhost 9090
```

# Final Code

I just sprinkled in some helpful messages, and added a bash [Shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)).

```bash
#!/usr/bin/env bash

while :
do
    echo "Attempting to connect to $1:$2 ..."

    if exec 7<> /dev/tcp/$1/$2 ; then
        echo "Connection Established to $1:$2"

        while :
        do
            if read -u 7 TITLE && read -u 7 DESCRIPTION ; then
                notify-send "$TITLE" "$DESCRIPTION"
            else
                break
            fi
        done
    fi

    echo "Connection lost. Retrying in 5 seconds ..."
    sleep 5
done
```   

It's a [gist with along with the server code](https://gist.github.com/omranjamal/cd7c456f323514f43f68512be9ab61f4) too if you need.

Don't forget to make the script executable before running.

```bash
chmod +x ./client.sh
```

## Test It

![Via Tenor](https://c.tenor.com/Ok29hcQos_wAAAAC/test-fail.gif)

Well, you could use [Insomnia](https://insomnia.rest/) or [Postman](https://www.postman.com/), but we love the CLI, so here are commands ...

```bash
# Start the server
node server.js

# Start the client
./client.sh localhost 9090

# Send the JSON payload with cURL
curl -X POST http://localhost:9091/ \
    -H 'Content-Type: application/json' \
    -d '{"title":"Testing","description":"This is a test."}'
```

## Running At Startup

To be honest, a whole host of things would be done to run programs at startup. Here are a few links ...

- [https://askubuntu.com/questions/814/how-to-run-scripts-on-start-up](https://askubuntu.com/questions/814/how-to-run-scripts-on-start-up) (every answer is a valid approach)
- [https://transang.me/three-ways-to-create-a-startup-script-in-ubuntu/](https://transang.me/three-ways-to-create-a-startup-script-in-ubuntu/)
- [https://stackoverflow.com/questions/6442364/running-script-upon-login-mac](https://stackoverflow.com/questions/6442364/running-script-upon-login-mac)

## Could This Be Improved?

Yes, in the following ways. (left as an exercise to the reader)

- Use a cli tool to sanitize whatever comes through the TCP socket.
- Introduce auth keys to be send as the first *INIT* message from client to server.
- Send messages symmetrically encrypted (or asymmetrically if you're bold enough) to stop eavesdropping.
- Setup authentication on the sending server.