NGINX as a Proxy for Websockets

NGINX supports WebSocket by allowing a tunnel to be set up between a client and a backend server. For NGINX to send the Upgrade request from the client to the backend server, the Upgrade and Connection headers must be set explicitly, as in this example:

location /wsapp/ {
    proxy_pass http://wsbackend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header Host $host;
}

Building the Websocket Server

Initialize your Node.js App and install the websocket package using npm:

npm init -y
npm install ws

Websocket Application

Create the server configuration:

./server.js

port = 8080
var Msg = '';
var WebSocketServer = require('ws').Server();
    wss = new WebSocketServer({port});
    wss.on('connection', function(ws) {
        ws.on('message', function(message) {
        console.log('Received from client: %s', message);
        ws.send('Server received from client: ' + message);
    });
 });
console.log("Websocket Server started on port " + port);

And execute the server with:

node server.js

NGINX Configuration

To have NGINX proxy the requests to our websocket app on the server IP 192.168.2.111, I create the following configuration:

/opt/websocket/wsserver/nginx.conf

worker_processes 1;
worker_rlimit_nofile 8192;

events {
  worker_connections  1024;
}

http {
    map $http_upgrade $connection_upgrade {
        default upgrade;
        '' close;
    }

    upstream websocket {
        server 192.168.2.111:8080;
    }

    server {
        listen 8300;
        location / {
            proxy_pass http://websocket;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
            proxy_set_header Host $host;
        }
    }
}

The map block sets the Connection header to close when the Upgrade header in the request is set to ”. NGINX listens on port 8300 and proxies requests to the backend WebSocket server on port 8080. The proxy_set_header directives enable NGINX to properly handle the WebSocket protocol.

Docker

I am going to use the NGINX docker image to spawn a container with the configuration above:

docker pull nginx:1.21-alpine
docker run -d --name nginx --network=host -v /opt/websocket/wsserver/nginx.conf:/etc/nginx/nginx.conf:ro nginx:1.21-alpine

WS Client

I can now install a websocket client called wscat. I can use the program to connect to the server:

npm install wscat -g

wscat --connect ws://192.168.2.111:8300
Connected (press CTRL+C to quit)
> Konbanwa!
< Server received from client: Konbanwa!
node server.js
Websocket Server started on Port 8080
Received from client: Konbanwa!

wscat connects to the WebSocket server through the NGINX proxy. When you type a message for wscat to send to the server, you see it echoed on the server and then a message from the server appears on the client!

Introduction to React.JS 2024

Creating Components

Create a file HelloThere.jsx in the sub directory components:

import React from "react";

// functional component
function HelloWorld() {
    return <h3>Hello World!</h3>
}

// arrow function
const ILikeArrows = () => {
    return <p>I am Artemis.</p>
}

// class component
class ClassyComponent extends React.Component {
    render() {
        return (
            <>
                <HelloWorld />
                <ILikeArrows />
            </>
        )
    }
}

export default ClassyComponent

This component can be imported into App.jsx and rendered by:

import ClassyComponent from './components/HelloThere'

function App() {

  return (
    <div className="App">
      <div>
        <ClassyComponent />
      </div>
      
      ...

    </div>
  )
}

export default App

Working with Props

// working with props
function Paragraph(props) {
    return (
    <p>In ancient { props.origin }, { props.name } is the { props.profession }. She was heavily identified with Selene, the personification of the Moon, and Hecate, another lunar deity, and was thus regarded as one of the most prominent lunar deities in mythology, alongside the aforementioned two. The goddess { props.equivalent } is her Roman equivalent.</p>
    )
}

function SelfIntroduction() {
    return (
    <>
        <Paragraph
            origin = "Greek mythology"
            name = "Artemis"
            profession = "goddess of the hunt"
            equivalent = "Diana" />
    </>
    )
}

export default SelfIntroduction

Content Projection

// content projection
function Card(props) {
    return (
    <div className="card">
        { props.children }
    </div>
    )
}

function Image() {
    const image = { url: "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Diana_of_Versailles.jpg/314px-Diana_of_Versailles.jpg" }

    return (
        
      <Card>
        <img src={ image.url } />
      </Card>
    );
  }

export default Image

Conditional Rendering

// functional component
function ConditionalImage({ goddess }) {

    const diana = { url: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/40/Jean-Fran%C3%A7ois_de_Troy_-_Diane_suprise_par_Act%C3%A9on%2C_1734.jpg/800px-Jean-Fran%C3%A7ois_de_Troy_-_Diane_suprise_par_Act%C3%A9on%2C_1734.jpg" }

    const artemis = { url: "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3e/Wall_painting_-_Artemis_and_Kallisto_-_Pompeii_%28VII_12_26%29_-_Napoli_MAN_111441.jpg/505px-Wall_painting_-_Artemis_and_Kallisto_-_Pompeii_%28VII_12_26%29_-_Napoli_MAN_111441.jpg" }

    if (goddess == "diana") {
        return <img src={ diana.url } />
    } else {
        return <img src={ artemis.url } />
    }
}

const ConditionalTitle = (props) => {
    return (
        <div className="title">
        {
            (props.goddess == 'diana')
            ? <h3>The Roman Goddess Diana</h3>
            : <h3>The Greek Goddess Artemis</h3>
        }
        </div>
    )
}


const PaintingCollection = () => {
    return (
        <>
            <h2>Paintings</h2>
            <ConditionalTitle goddess='artemis' />
            <ConditionalImage goddess='artemis' />
            <ConditionalTitle goddess='diana' />
            <ConditionalImage goddess='diana' />
        </>
    )
}

export default PaintingCollection

Loops

const ListItem = (props) => {
    return <li>{props.name}</li>
}

function ListOfAliases() {

    const data = [
        {id: 1, goddess: 'Artemis'},
        {id: 2, goddess: 'Diana'}
    ]

    return (
        <>
            <h3>List of Aliases</h3>
            <ul>
                { data.map(({ id, goddess }) =>
                    <ListItem key={id} name={goddess} />
                )}
            </ul>
        </>
    )
}

export default ListOfAliases

Events

function EventCatcher() {

    const clickHandler = (event) => {
        console.log(event)
    }

    return (
        <>
            <button onClick={clickHandler}>Log Click-Event</button>
        </>
    )
}

export default EventCatcher
function ActionButton({ onClick }) {
    return <button onClick={onClick}>Log Click-Event</button>
}

function EventCatcher() {

    const clickHandler = (event) => {
        console.log(event)
    }

    return <ActionButton onClick={ clickHandler } />
}

export default EventCatcher

State and Reducer Hooks

import { useState, useReducer } from 'react'

const initialState = {count: 0}

function reducer(reducedState, action) {
    switch (action.type) {
        case 'increment':
            return { count: reducedState.count + 1 }
        case 'decrement':
            return { count: reducedState.count - 1 }
        default:
            throw new Error()
    }
}


// props are immutable to be able to modify
// state and redraw ui components use a state hook
function Stateful() {
    const [count, setCount] = useState(0)
    const [state, setState] = useState({ count: 0, goddess: 'Artemis'})

    const handleClick = () => {
        setState({
            ...state,
            count: state.count + 1,
        })
    }

    const [reducedState, dispatch] = useReducer(reducer, initialState)
    
    return (
        <>
            <p>Diana: { count }</p>
            <button onClick={ () => setCount(count +1)}>Like</button>
            <p>{ state.goddess }: { state.count }</p>
            <button onClick={ handleClick }>Like</button>
            <p>God in general: { reducedState.count }</p>
            <button onClick={ () => dispatch({type: 'increment'})}>Like</button>
            <button onClick={ () => dispatch({type: 'decrement'})}>Dislike</button>
        </>
    )
}

export default Stateful

Effects – Lifecycle for Functional Components

No Dependencies

useEffect(() => {
  //Runs on every render
});
import { useState, useEffect } from "react";

function Timer() {
    const [count, setCount] = useState(1000);
  
    useEffect(() => {
      setTimeout(() => {
        setCount((count) => count - 1);
      }, 1000);
    });
  
    return <h1>Countdown: {count}s</h1>;
  }

  export default Timer

An empty Array

Update when component did mount:

useEffect(() => {
  //Runs only on the first render
}, []);

Props or State Values

Update when component did update:

useEffect(() => {
  //Runs on the first render
  //And any time any dependency value changes
}, [prop, state]);
import { useState, useEffect } from "react"

function Counter() {
  const [count, setCount] = useState(0);
  const [calculation, setCalculation] = useState(0);

  useEffect(() => {
    setCalculation(() => Math.pow(count, 2));
  }, [count]); // <- add the count variable here

  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}>Add Awesomeness</button>
      <p>Awesomeness: {count}</p>
      <p>Awesomeness²: {calculation}</p>
    </>
  );
}

export default Counter

Unmount Legacy

Update when component did unmount:

useEffect(() => {
  //Runs when component get's destroyed
  return () => console.log('Ciao!')
}, [prop, state]);

Context API – Share Reactive Data among Components

// without context
// pass prop down a chain of child components

function PropDrilling() {
    const [count] = useState(44)

    return <Child count={count} />
}

function Child({ count }) {
    return <GrandChild count={count} />
}

function GrandChild({ count }) {
    return <div>{count}</div>
}
// with context
// write prop to context to make it available to all components

const CountContext = createContext(0)

function PropContext() {
    const [count] = useState(88)

    return (
        <CountContext.Provider value={count}>
            <ChildContext count={count} />
        </CountContext.Provider>
    )
}

function ChildContext() {
    return <GrandChildContext />
}

function GrandChildContext() {
    const count = useContext(CountContext)
    return <div>{count}</div>
}

Error Boundary

class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props)
        this.state = { hasError: false }
    }

    // catch error
    static getDerivedStateFromError(error) {
        return { hasError: true }
    }

    componentDidCatch(error, errorInfo) {
        console.log(error, errorInfo)
    }

    render() {
        // there was a problem - provide fallback
        if (this.state.hasError) {
            return <p><em>Safe Fallback instead of a Crash</em></p>
        }
        // everything is fine - continue
        return this.props.children
    }
}


function BreakingComponent() {
    return <p>{ anelusiveprop }</p>
}

function CaughtError() {
    
    return (
        <>
            <ErrorBoundary>
                <BreakingComponent />
            </ErrorBoundary>
        </>
    )
  }

export default CaughtError

Introduction to Websockets

TCP & UDP in Node.js

TCP Server

const net = require("net")

const port = 8080

const server = net.createServer(socket => {
    socket.write("Hello.")
    socket.on("data", data => {
        console.log(data.toString())
    })
})

server.listen(port)

You can run this file in Node.js:

node tcp.js

And connect to it using Telnet:

telnet 127.0.0.1 8080
Hello

Everything you type after that will be logged in your server console.

UDP Server

const dgram = require('dgram');
const socket = dgram.createSocket('udp4');

const port = 8081

socket.on('message', (msg, rinfo) => {
    console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`);
});

socket.bind(port);
echo "hi" | nc -w1 -u 192.168.2.112 8081
server got: hi
from 192.168.2.110:44757

HTTP in Node.js

var http = require('http')
var url = require('url')
var fs = require('fs')
var path = require('path')
var baseDirectory = __dirname   // or whatever base directory you want

var port = 80

http.createServer(function (request, response) {
    try {
        var requestUrl = url.parse(request.url)

        // need to use path.normalize so people can't access directories underneath baseDirectory
        var fsPath = baseDirectory+path.normalize(requestUrl.pathname)
        var fileStream = fs.createReadStream(fsPath)
        fileStream.pipe(response)

        fileStream.on('open', function() {
             response.writeHead(200)
        })

        fileStream.on('error',function(e) {
             response.writeHead(404)     // assume the file doesn't exist
             response.end()
        })
   } catch(e) {
        response.writeHead(500)
        response.end()     // end the response so browsers don't hang
        console.log(e.stack)
   }
}).listen(port)

console.log("listening on port "+port)
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content= "width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title>A HTML Page</title>
    </head>
    <body>
        <h1>Just a Test</h1>
    </body>
</html>

Run node http.js and open the page inside your web browser or run the following from your Terminal:

curl htttp://localhost/index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content= "width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title>A HTML Page</title>
    </head>
    <body>
        <h1>Just a Test</h1>
    </body>
</html>

Websockets in Node.js

Building a Websocket server in Node.js – starting by creating a basic http /tcp server:

const http = require("http")
const port = 8888

const server = http.createServer((req, res) => {
    console.log("Request received")
})

server.listen(port, () => console.log("Websocket Server listening on Port "+port))

Run the server with Node.js and try sending a GET request from your browser on http://localhost:8888/ – the line Request received should appear every time you send an request:

node index.js
Websocket Server listening on Port 8888
Request received

The server now is able to receive HTTP GET request. I now want to upgrade incoming requests to a websocket connection:

const http = require("http")
const WebSocket = require("websocket").server
let connection = null;
const port = 8888

const server = http.createServer((req, res) => {
    console.log("Request received")
})

const websocket = new WebSocket({
    "httpServer": server
})

websocket.on("request", request=> {
    connection = request.accept(null, request.origin);
    connection.on("open", () => console.log("Websocket connection established"))
    connection.on("message", message => {
        console.log(`message: ${message.utf8Data}`)
    })
    connection.on("close", () => console.log("Websocket connection closed"))
})

server.listen(port, () => console.log("Websocket Server listening on Port "+port))

Install the websocket module and re-run the server – sending an HTTP GET request should now initialize the websocket connection:

npm init -y
npm install websocket
node index.js

And I can use my browser console as the client application and send a message to my server:

ws = new WebSocket("ws://localhost:8888")
ws.onmessage = message => console.log(`${message.data}`)
ws.send("Hi")

To send a message from the Server to the Client I can add another to the server script:

const http = require("http");
const WebSocket = require("websocket").server;
let connection = null;
const port = 8888;

const server = http.createServer((req, res) => {
    console.log("Request received")
});

const websocket = new WebSocket({
    "httpServer": server
});

websocket.on("request", request=> {
    connection = request.accept(null, request.origin);
    connection.on("open", () => console.log("Websocket connection established"))
    connection.on("message", message => {
        console.log(`message: ${message.utf8Data}`)
    })
    connection.on("close", () => console.log("Websocket connection closed"))

    heartBeat();
});

server.listen(port, () => console.log("Websocket Server listening on Port "+port));

function heartBeat(){
    connection.send(`I am still here`);
    setTimeout(heartBeat, 30000);
};

Web3.js Blockchain Application

Web3.js is a collection of libraries that allow you to interact with a local or remote Ethereum node using HTTP, IPC, or WebSocket. Web3.js can be used in the frontend or backend of an application to read data from the blockchain.

Blockchain authentication can be used to protect application with public data. It helps identify who is accessing the website and what data should be accessible for the logged-in user. This is the evolution coming from a Basic Authentication system with a username and password in web1 to OAuth system allowing users to use their social media logins to login to another system with web2 and on to web3 – a decentralized system that does not involve a third-party that holds your data for you. Every user has complete control over their personal data using a Decentralised Identity.

As a proof of work we can build an application that:

  • Has a login section where the user can connect to their wallet.
  • Has a dashboard section to display the logged-in user’s wallet address and their Ethereum account balance.

Web3.js Library

The Web3 library can be installed as a Node module. But to get started it can also be linked in from a CDN:

<script src="https://cdnjs.cloudflare.com/ajax/libs/web3/1.8.1/web3.min.js" integrity="sha512-vtUOC0YIaNm/UutU7yfnwqnF9LOYnXtpHe2kwi1nvJNloeGQuncNBiEKP/3Ww3D62USAhbXGsnYpAYoiDsa+wA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

Update: There is now a new alpha version 4 available.

<script src="https://cdnjs.cloudflare.com/ajax/libs/web3/4.0.1-alpha.1/web3.min.js" integrity="sha512-53NYeuuMZACjFqIMYEeKpIAURC/HYaAKFQ3InNgTmS45BX7qNOMmjgmE5Fvidj0+hHHC9ICYi5L4AAIhqG2fOQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/web3/4.0.1-alpha.1/web3.min.js"
            integrity="sha512-53NYeuuMZACjFqIMYEeKpIAURC/HYaAKFQ3InNgTmS45BX7qNOMmjgmE5Fvidj0+hHHC9ICYi5L4AAIhqG2fOQ=="
            crossorigin="anonymous"
            referrerpolicy="no-referrer"></script>
    <link rel="stylesheet" href="styles.css" />
    <title>Web3.js Login Test</title>
  </head>
  <body></body>
  <script src="./scripts.js"></script>
</html>

Ethereum Wallet

To be able to authenticate using the Ethereum Blockchain we need to setup a Ethereum Wallet on our device first. We can use Metamask to interact with our Ethereum account via a web browser – e.g. as a Chrome Plugin:

Create your account and generate your Secret Key and keep it somewhere save:

Building the Application

Now that we have Web3.js and an Ethereum wallet installed we can start building the interface. Add the following blocks inside the Body tag.

The Login Screen

index.html

<section class="login-section">
    <button class="login-btn">🔓 Log in with Web3</button>
    <span class="instruction">
        Ensure to have an Ethereum based wallet installed i.e MetaMask
    </span>
</section>

The Dashboard

index.html

<!-- DASHBOARD SECTION -->
<section class="dashboard-section">
    <h2 class="wallet-status">Wallet Connected! 🤝</h2>
    <h3 class="wallet-address-heading">
        ETH Wallet Address:
        <span class="wallet-address"></span>
    </h3>
    <h3 class="wallet-balance-heading">
        ETH Balance:
        <span class="wallet-balance"></span>
    </h3>
    <button class="logout-btn">🔐 Log out</button>
</section>

Scripting

Do you even have an Ethereum Wallet?

On page load, check if the user has an Ethereum wallet installed. Else prompt the user with a message to install a wallet. If the user is already logged in simply load the dashboard:

scripts.js

// Global wallet address variable
window.userWalletAddress = null;

// Check if metamask is available and if user is logged in already
window.onload = async (event) => {

  // Check if ethereum extension is installed
  if (window.ethereum) {
    // Create instance
    window.web3 = new Web3(window.ethereum);
  } else {
    // Prompt to install
    alert("Please install MetaMask or any Ethereum Extension Wallet");
  }
  // Check if already logged in and update the global userWalletAddress variable
  window.userWalletAddress = window.localStorage.getItem("userWalletAddress");
  // Show dashboard
  showUserDashboard();
};

Login Button

When the Metamask extension is installed but the user is not yet logged in we need a login function. Bind the login button to this function:

scripts.js

// Web3 login function
const loginWithEth = async () => {
    // Check if there is global instance
    if (window.web3) {
      try {
        // Get the user's account
        const selectedAccount = await window.ethereum
          .request({
            // Prompt metamask to login
            method: "eth_requestAccounts",
          })
          .then((accounts) => accounts[0])
          .catch(() => {
            // Catch cancelled login
            throw Error("Please select an account");
          });
  
        // Set global wallet address variable
        window.userWalletAddress = selectedAccount;
  
        // Store in local storage => can be moved to db later
        window.localStorage.setItem("userWalletAddress", selectedAccount);
  
        // Show dashboard
        showUserDashboard();
  
      } catch (error) {
        alert(error);
      }
    } else {
      alert("Wallet not found");
    }
  };
  
  // Bind function to login button
  document.querySelector(".login-btn").addEventListener("click", loginWithEth);

Load Dashboard after Login

Check if a user is connected to their wallet address. If they are not connected, display the login section. Once connected redirect the user to the dashboard section:

// Show the user dashboard
const showUserDashboard = async () => {

    // If not logged in
    if (!window.userWalletAddress) {
  
      // Change the page title
      document.title = "Web3 Login";
  
      // Show the login
      document.querySelector(".login-section").style.display = "flex";
  
      // Hide dashboard
      document.querySelector(".dashboard-section").style.display = "none";
      return false;
    }
  
    // change the page title
    document.title = "Metamask Dashboard";
  
    // Hide the login
    document.querySelector(".login-section").style.display = "none";
  
    // Show dashboard
    document.querySelector(".dashboard-section").style.display = "flex";
};

Displaying Ethereum Stats

// Show wallet address from stored variable
const showUserWalletAddress = () => {
  const walletAddressEl = document.querySelector(".wallet-address")
  walletAddressEl.innerHTML = window.userWalletAddress
}

// Get wallet balance
const getWalletBalance = async () => {
    if (!window.userWalletAddress) {
      return false
    }
    const balance = await window.web3.eth.getBalance(window.userWalletAddress)
    // Convert the balance to ether
    document.querySelector(".wallet-balance").innerHTML = web3.utils.fromWei(
      balance,
      "ether"
    )
}

Web3 Logout

To logout we need to set the global userWalletAddress variable to null and remove the userWalletAddress from the browser localStorage:

// Web3 logout
const logout = () => {
  // Set the global userWalletAddress to null
  window.userWalletAddress = null

  // Remove wallet address from local storage
  window.localStorage.removeItem("userWalletAddress")

  // Show the user dashboard
  showUserDashboard()
}

// Bind function to logout button
document.querySelector(".logout-btn").addEventListener("click", logout)