Share files securely peer to peer with WEBRTC

Share files securely peer to peer with WEBRTC

Hello fellow developers, Have you ever wonder if you can share file without a server directly to another peer and realtime? Yes you can and here Ι will show you how to do it.

All code from this tutorial as a complete package is available in GitHub in this repository

The server Oh ok this is awkward, I just said before "without server" but you need a server just for signaling. How the other peer will find your computer among billion of devices in the internet?

Initialize a new node.js project: npm init

Install express: npm install --save express

Install socket.io for signaling: npm install --save socket.io

Install ejs for server side rendering: npm install --save ejs

Server code: server.js This is the simplest server for signaling in order to connect max two peers and one view:

//Loading dependencies & initializing express
var os = require('os');
var express = require('express');
var app = express();
var http = require('http');
//For signalling in WebRTC
var socketIO = require('socket.io');


app.use(express.static('public'))

app.get("/", function(req, res){
    res.render("index.ejs");
});

var server = http.createServer(app);

server.listen(process.env.PORT || 3000);

var io = socketIO(server);

io.sockets.on('connection', function(socket) {

    // Convenience function to log server messages on the client.
    // Arguments is an array like object which contains all the arguments of log(). 
    // To push all the arguments of log() in array, we have to use apply().
    function log() {
      var array = ['Message from server:'];
      array.push.apply(array, arguments);
      socket.emit('log', array);
    }
  
    
    //Defining Socket Connections
    socket.on('message', function(message, room) {
      log('Client said: ', message);
      // for a real app, would be room-only (not broadcast)
      socket.in(room).emit('message', message, room);
    });
  
    socket.on('create or join', function(room) {
      log('Received request to create or join room ' + room);
  
      var clientsInRoom = io.sockets.adapter.rooms.get(room);

      var numClients = clientsInRoom ? clientsInRoom.size : 0;
      log("Room " + room + " now has " + numClients + " client(s)")
  
      if (numClients === 0) {
        socket.join(room);
        log('Client ID ' + socket.id + ' created room ' + room);
        socket.emit('created', room, socket.id);
  
      } else if (numClients === 1) {
        log('Client ID ' + socket.id + ' joined room ' + room);
        io.sockets.in(room).emit('join', room);
        socket.join(room);
        socket.emit('joined', room, socket.id);
        io.sockets.in(room).emit('ready');
      } else { // max two clients
        socket.emit('full', room);
      }
    });
  
    socket.on('ipaddr', function() {
      var ifaces = os.networkInterfaces();
      for (var dev in ifaces) {
        ifaces[dev].forEach(function(details) {
          if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
            socket.emit('ipaddr', details.address);
          }
        });
      }
    });
  
    socket.on('bye', function(){
      console.log('received bye');
    });
  
  });

Code explanation

  • At line 12 we use express and ejs to render a simple html page that will help us to make the UI for the file sharing.

  • At line 16 a http server is created and is set to listen to port 3000 or the port defined by a PORT environment variable at line 18

  • At line 20 a new instance of socket.io is initialized and we pass http server as a parameter.

  • At line 22 we listen at the connection event where we have defined all our other events that occur after connection.

  • function log() is a function that emits to the client who sent the message a log event and some message that are defined in the arguments as an array

Lets talk about sockets events that are defined inside our server code.

  • create or join when the server receives this event from the client initially at line 44 it gets the number of clients in room. and stores it in var clientsInRoom, if this variable is null then we define the numClients as zero otherwise we set it as the clientsInRoom. At line 49 if the clients who sends the message is the first then we emit the created event to the client otherwise at line 54 if is the second who sends this message we send join to the first client, we join the second client and a joined event is sent to the second client that just joined the room. At the end the two clients will receive ready event. If anyone else except these two clients send other create or join event he will receive a full event.

  • ipaddr event is for signaling webRTC

For the client your will need a config.js file which contains a json object for STUN/TURN server. I will not explain in details what is this for in this article but it is necessary to help two clients to exchange their public IP Adresses. The gist above is not correct for security reasons but you can create your own config object at xirsys

const turnConfig = {
    iceServers: [
      { urls: ["stun:fr-turn1.xirsys.com"] },
      {
        username:
          "WkcsdyTovJl_655q7_9Y-Uy_DTmagjhkdfcbadkjscfbdsakclsdGlvO4exr4KnAAAAAGGYE3dlbmVhc2xhcmk=",
        credential: "8a1494ghge0-4887797d-11ec-9fcf-0242aghngf20004",
        urls: [
          "turn:fr-turn1.xirsys.com:80?transport=udp",
          "turn:fr-turn1.xirsys.com:3478?transport=udp",
          "turn:fr-turn1.xirsys.com:80?transport=tcp",
          "turn:fr-turn1.xirsys.com:3478?transport=tcp",
          "turns:fr-turn1.xirsys.com:443?transport=tcp",
          "turns:fr-turn1.xirsys.com:5349?transport=tcp",
        ],
      },
    ],
  };
  

Client code for establishing a peer connection:

//Defining some global utility variables
var isChannelReady = false;
var isInitiator = false;
var isStarted = false;
var pc;
var turnReady;

//Initialize turn/stun server here
//turnconfig will be defined in public/js/config.js
var pcConfig = turnConfig;


// Prompting for room name:
var room = prompt('Enter room name:');

//Initializing socket.io
var socket = io.connect();

//Ask server to add in the room if room name is provided by the user
if (room !== '') {
  socket.emit('create or join', room);
  console.log('Attempted to create or  join room', room);
}

//Defining socket events

//Event - Client has created the room i.e. is the first member of the room
socket.on('created', function(room) {
  console.log('Created room ' + room);
  isInitiator = true;
});

//Event - Room is full
socket.on('full', function(room) {
  console.log('Room ' + room + ' is full');
});

//Event - Another client tries to join room
socket.on('join', function (room){
  console.log('Another peer made a request to join room ' + room);
  console.log('This peer is the initiator of room ' + room + '!');
  isChannelReady = true;
});

//Event - Client has joined the room
socket.on('joined', function(room) {
  console.log('joined: ' + room);
  isChannelReady = true;
});

//Event - server asks to log a message
socket.on('log', function(array) {
  console.log.apply(console, array);
});


//Event - for sending meta for establishing a direct connection using WebRTC
//The Driver code
socket.on('message', function(message, room) {
    console.log('Client received message:', message,  room);
    if (message === 'gotuser') {
      maybeStart();
    } else if (message.type === 'offer') {
      if (!isInitiator && !isStarted) {
        maybeStart();
      }
      pc.setRemoteDescription(new RTCSessionDescription(message));
      doAnswer();
    } else if (message.type === 'answer' && isStarted) {
      pc.setRemoteDescription(new RTCSessionDescription(message));
    } else if (message.type === 'candidate' && isStarted) {
      var candidate = new RTCIceCandidate({
        sdpMLineIndex: message.label,
        candidate: message.candidate
      });
      pc.addIceCandidate(candidate);
    } else if (message === 'bye' && isStarted) {
      handleRemoteHangup();
    }
});
  


//Function to send message in a room
function sendMessage(message, room) {
  console.log('Client sending message: ', message, room);
  socket.emit('message', message, room);
}



//If initiator, create the peer connection
function maybeStart() {
  console.log('>>>>>>> maybeStart() ', isStarted,  isChannelReady);
  if (!isStarted && isChannelReady) {
    console.log('>>>>>> creating peer connection');
    createPeerConnection();
    isStarted = true;
    console.log('isInitiator', isInitiator);
    if (isInitiator) {
      doCall();
    }
  }
}

//Sending bye if user closes the window
window.onbeforeunload = function() {
  sendMessage('bye', room);
};


//Creating peer connection
function createPeerConnection() {
  try {
    pc = new RTCPeerConnection(pcConfig);
    pc.onicecandidate = handleIceCandidate;
    console.log('Created RTCPeerConnnection');
  } catch (e) {
    console.log('Failed to create PeerConnection, exception: ' + e.message);
    alert('Cannot create RTCPeerConnection object.');
    return;
  }
}

//Function to handle Ice candidates generated by the browser
function handleIceCandidate(event) {
  console.log('icecandidate event: ', event);
  if (event.candidate) {
    sendMessage({
      type: 'candidate',
      label: event.candidate.sdpMLineIndex,
      id: event.candidate.sdpMid,
      candidate: event.candidate.candidate
    }, room);
  } else {
    console.log('End of candidates.');
  }
}

function handleCreateOfferError(event) {
  console.log('createOffer() error: ', event);
}

//Function to create offer
function doCall() {
  console.log('Sending offer to peer');
  pc.createOffer(setLocalAndSendMessage, handleCreateOfferError);
}

//Function to create answer for the received offer
function doAnswer() {
  console.log('Sending answer to peer.');
  pc.createAnswer().then(
    setLocalAndSendMessage,
    onCreateSessionDescriptionError
  );
}

//Function to set description of local media
function setLocalAndSendMessage(sessionDescription) {
  pc.setLocalDescription(sessionDescription);
  console.log('setLocalAndSendMessage sending message', sessionDescription);
  sendMessage(sessionDescription, room);
}

function onCreateSessionDescriptionError(error) {
  trace('Failed to create session description: ' + error.toString());
}


function hangup() {
  console.log('Hanging up.');
  stop();
  sendMessage('bye',room);
}

function handleRemoteHangup() {
  console.log('Session terminated.');
  stop();
  isInitiator = false;
}

function stop() {
  isStarted = false;
  pc.close();
  pc = null;
}

var connectbutton = document.getElementById("connectbutton")
if (connectbutton) {
    connectbutton.addEventListener("click", () => {
        if (connectbutton.innerHTML !== "Connected") {
            socket.emit("create or join", room);
            sendMessage("gotuser", room);
            if (isInitiator) {
                maybeStart();
            }
        }
        connectbutton.innerHTML = "Connected";
        //connection logic
    })
}

And the html page:

<!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">
    <title>Document</title>
</head>

<body>
    <h1>Send files peer to peer</h1>


    <button id="connectbutton">Connect with peer</button>

    <!-- Import SocketIO for signalling -->
    <script src="/socket.io/socket.io.js"></script>

    <!-- Import WebRTC adapter for compatibility with all the browsers  -->
    <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

    <script src="js/config.js"></script>
    <script src="js/main.js"></script>
</body>

</html>

If you run the server as it is with node server.js

and visit

http://localhost:3000/

you will see in the debug window in browser:

Image description

If you open a second window and add the same room name you will see through console that an peer connection is established

Image description

After connection:

Image description

Datachannel on peer connection Now we are ready to create the datachannel for file trasfer And then send the file to the other peer when the user clicks the send button.

The updated 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">
    <title>Document</title>
</head>

<body>
    <h1>Send files peer to peer</h1>


    <button id="connectbutton">Connect with peer</button>
    <input id="inputfile" type="file">
    <button id="sharefile">Share file</button>

    <!-- Import SocketIO for signalling -->
    <script src="/socket.io/socket.io.js"></script>

    <!-- Import WebRTC adapter for compatibility with all the browsers  -->
    <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

    <script src="js/config.js"></script>
    <script src="js/main.js"></script>
</body>

</html>

and finally the updated javascript file:

//Defining some global utility variables
var isChannelReady = false;
var isInitiator = false;
var isStarted = false;
var pc;
var turnReady;
var datachannel;

//Initialize turn/stun server here
//turnconfig will be defined in public/js/config.js
var pcConfig = turnConfig;


// Prompting for room name:
var room = prompt('Enter room name:');

//Initializing socket.io
var socket = io.connect();

//Ask server to add in the room if room name is provided by the user
if (room !== '') {
    // socket.emit('create or join', room);
    // console.log('Attempted to create or  join room', room);
}

//Defining socket events

//Event - Client has created the room i.e. is the first member of the room
socket.on('created', function (room) {
    console.log('Created room ' + room);
    isInitiator = true;
});

//Event - Room is full
socket.on('full', function (room) {
    console.log('Room ' + room + ' is full');
});

//Event - Another client tries to join room
socket.on('join', function (room) {
    console.log('Another peer made a request to join room ' + room);
    console.log('This peer is the initiator of room ' + room + '!');
    isChannelReady = true;
});

//Event - Client has joined the room
socket.on('joined', function (room) {
    console.log('joined: ' + room);
    isChannelReady = true;
});

//Event - server asks to log a message
socket.on('log', function (array) {
    console.log.apply(console, array);
});


//Event - for sending meta for establishing a direct connection using WebRTC
//The Driver code
socket.on('message', function (message, room) {
    console.log('Client received message:', message, room);
    if (message === 'gotuser') {
        maybeStart();
    } else if (message.type === 'offer') {
        if (!isInitiator && !isStarted) {
            maybeStart();
        }
        pc.setRemoteDescription(new RTCSessionDescription(message));
        doAnswer();
    } else if (message.type === 'answer' && isStarted) {
        pc.setRemoteDescription(new RTCSessionDescription(message));
    } else if (message.type === 'candidate' && isStarted) {
        var candidate = new RTCIceCandidate({
            sdpMLineIndex: message.label,
            candidate: message.candidate
        });
        pc.addIceCandidate(candidate);
    } else if (message === 'bye' && isStarted) {
        handleRemoteHangup();
    }
});



//Function to send message in a room
function sendMessage(message, room) {
    console.log('Client sending message: ', message, room);
    socket.emit('message', message, room);
}



//If initiator, create the peer connection
function maybeStart() {
    console.log('>>>>>>> maybeStart() ', isStarted, isChannelReady);
    if (!isStarted && isChannelReady) {
        console.log('>>>>>> creating peer connection');
        createPeerConnection();
        isStarted = true;
        console.log('isInitiator', isInitiator);
        if (isInitiator) {
            doCall();
        }
    }
}

//Sending bye if user closes the window
window.onbeforeunload = function () {
    sendMessage('bye', room);
};
var datachannel
//Creating peer connection
function createPeerConnection() {
    try {
        pc = new RTCPeerConnection(pcConfig);
        pc.onicecandidate = handleIceCandidate;
        console.log('Created RTCPeerConnnection');

        // Offerer side
        datachannel = pc.createDataChannel("filetransfer");
        datachannel.onopen =  (event) => {
            datachannel.send("oferer sent:THIS")
        };

        datachannel.onmessage =  (event)=> {
            console.log("The oferrer received a message"+event.data);
        }
        datachannel.onerror = (error) => {
            //console.log("Data Channel Error:", error);
        };

        datachannel.onclose = () => {
            //console.log("Data Channel closed");
        };


        // Answerer side
        pc.ondatachannel = function (event) {
            var channel = event.channel;
            channel.onopen = function (event) {
                channel.send('ANSWEREROPEN');
            }

            const receivedBuffers = [];
            channel.onmessage = async (event) => {
                console.log("The Answerrer received a message"+event.data);
                const { data } = event;
                try {
                    if (data !== END_OF_FILE_MESSAGE) {
                        receivedBuffers.push(data);
                    } else {
                        const arrayBuffer = receivedBuffers.reduce((acc, arrayBuffer) => {
                            const tmp = new Uint8Array(acc.byteLength + arrayBuffer.byteLength);
                            tmp.set(new Uint8Array(acc), 0);
                            tmp.set(new Uint8Array(arrayBuffer), acc.byteLength);
                            return tmp;
                        }, new Uint8Array());
                        const blob = new Blob([arrayBuffer]);
                        channel.send("THE FILE IS READYYY")
                        downloadFile(blob, channel.label);
                        channel.close();
                    }
                } catch (err) {
                    console.log('File transfer failed');
                }
            };
        };
    } catch (e) {
        console.log('Failed to create PeerConnection, exception: ' + e.message);
        alert('Cannot create RTCPeerConnection object.');
        return;
    }
}

//Function to handle Ice candidates generated by the browser
function handleIceCandidate(event) {
    console.log('icecandidate event: ', event);
    if (event.candidate) {
        sendMessage({
            type: 'candidate',
            label: event.candidate.sdpMLineIndex,
            id: event.candidate.sdpMid,
            candidate: event.candidate.candidate
        }, room);
    } else {
        console.log('End of candidates.');
    }
}

function handleCreateOfferError(event) {
    console.log('createOffer() error: ', event);
}

//Function to create offer
function doCall() {
    console.log('Sending offer to peer');
    pc.createOffer(setLocalAndSendMessage, handleCreateOfferError);
}

//Function to create answer for the received offer
function doAnswer() {
    console.log('Sending answer to peer.');
    pc.createAnswer().then(
        setLocalAndSendMessage,
        onCreateSessionDescriptionError
    );
}

//Function to set description of local media
function setLocalAndSendMessage(sessionDescription) {
    pc.setLocalDescription(sessionDescription);
    console.log('setLocalAndSendMessage sending message', sessionDescription);
    sendMessage(sessionDescription, room);
}

function onCreateSessionDescriptionError(error) {
    trace('Failed to create session description: ' + error.toString());
}


function hangup() {
    console.log('Hanging up.');
    stop();
    sendMessage('bye', room);
}

function handleRemoteHangup() {
    console.log('Session terminated.');
    stop();
    isInitiator = false;
}

function stop() {
    isStarted = false;
    pc.close();
    pc = null;
}


var connectbutton = document.getElementById("connectbutton")
if (connectbutton) {
    connectbutton.addEventListener("click", () => {
        if (connectbutton.innerHTML !== "Connected") {
            socket.emit("create or join", room);
            sendMessage("gotuser", room);
            if (isInitiator) {
                maybeStart();
            }
        }
        connectbutton.innerHTML = "Connected";
        //connection logic
    })
}

let file;
//DOM elements
var fileInput = document.getElementById("inputfile");
fileInput.addEventListener("change", (event) => {
    file = event.target.files[0];
})
var sharefilebutton = document.getElementById("sharefile")
sharefilebutton.addEventListener("click", () => {
    shareFile()
})

const MAXIMUM_MESSAGE_SIZE = 65535;
const END_OF_FILE_MESSAGE = 'EOF';

const shareFile = async () => {
    console.log("Share file")
    if (file) {
        const arrayBuffer = await file.arrayBuffer();
        console.log(arrayBuffer)
        for (let i = 0; i < arrayBuffer.byteLength; i += MAXIMUM_MESSAGE_SIZE) {
            datachannel.send(arrayBuffer.slice(i, i + MAXIMUM_MESSAGE_SIZE));
        }
        datachannel.send(END_OF_FILE_MESSAGE);
    }
};

const downloadFile = (blob, fileName) => {
    const a = document.createElement('a');
    const url = window.URL.createObjectURL(blob);
    a.href = url;
    a.download = fileName;
    a.click();
    window.URL.revokeObjectURL(url);
    a.remove()
};

Complete working project: GitHub repo: link

Any comments or corrections are welcome and appreciated!

Thank you for reading!

More to read