Skip to content

Commit

Permalink
LOTS of stuff!!
Browse files Browse the repository at this point in the history
- Refactor much
- multi file transfers
- multi webrtc sessions on just one WS
- manifest.json update
- update signalling server
  • Loading branch information
robinkarlberg committed Jan 19, 2024
1 parent c4bd0bb commit 1cc96c5
Show file tree
Hide file tree
Showing 7 changed files with 774 additions and 676 deletions.
35 changes: 25 additions & 10 deletions signaling-server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ const wss = new WebSocketServer({

const sessions = new Map();

const deleteSession = sessionId => {
console.log("Deleting session:", sessionId)
sessions.delete(sessionId);
}

wss.on("connection", conn => {
console.log("conn");

Expand All @@ -20,10 +25,10 @@ wss.on("connection", conn => {
});

conn.on("close", () => {
console.log("Connection closed")
if (conn._session?.ids) {
for (let id of conn._session.ids) {
sessions.delete(id);
console.log("Connection closed, deleted session ", id)
deleteSession(id)
}
}
else {
Expand Down Expand Up @@ -75,7 +80,7 @@ function handleMessage(conn, message) {

sessions.set(data.id, conn);

return conn.send(JSON.stringify({ success: true, type: data.type }));
return conn.send(JSON.stringify({ targetId: data.id, success: true, type: data.type }));
}

if (!conn._session) return conn.close(); // client has to send type 0 first >:(
Expand All @@ -90,16 +95,17 @@ function handleMessage(conn, message) {
if ((recipientConn = sessions.get(data.recipientId))) {
recipientConn.send(JSON.stringify({
type: 11, // offer type
targetId: data.recipientId,
callerId: data.callerId,
offer: data.offer,
}));
return conn.send(JSON.stringify({
success: true, type: data.type,
targetId: data.callerId, success: true, type: data.type,
}));
} else {
console.log("Recipient did not exist!")
console.log("Recipient did not exist:", data.recipientId)
return conn.send(JSON.stringify({
success: false, type: data.type,
targetId: data.callerId, success: false, type: data.type,
msg: "recipient does not exist",
}));
}
Expand All @@ -111,15 +117,16 @@ function handleMessage(conn, message) {
if ((recipientConn = sessions.get(data.recipientId))) {
recipientConn.send(JSON.stringify({
type: 12, // answer type
targetId: data.recipientId,
answer: data.answer,
}));
return conn.send(JSON.stringify({
success: true, type: data.type,
targetId: data.sessionId, success: true, type: data.type,
}));
} else {
console.log("Recipient did not exist!")
return conn.send(JSON.stringify({
success: false, type: data.type,
targetId: data.sessionId, success: false, type: data.type,
msg: "recipient does not exist",
}));
}
Expand All @@ -131,17 +138,25 @@ function handleMessage(conn, message) {
if ((recipientConn = sessions.get(data.recipientId))) {
recipientConn.send(JSON.stringify({
type: 13, // answer type
targetId: data.recipientId,
candidate: data.candidate,
}));
return conn.send(JSON.stringify({
success: true, type: data.type,
targetId: data.sessionId, success: true, type: data.type,
}));
} else {
console.log("Recipient did not exist!")
return conn.send(JSON.stringify({
success: false, type: data.type,
targetId: data.sessionId, success: false, type: data.type,
msg: "recipient does not exist",
}));
}
} else if (data.type == 4) { // logout
// console.log("candidate", conn._session.id + " -> " + data.recipientId, data);
if (!data.sessionId) return closeConnWithReason(conn, "[logout] Didn't specify sessionId")
if (!conn._session.ids.find(o => o === data.sessionId))
return closeConnWithReason(conn, "[logout] Tried to logout id not owned by them");

deleteSession(data.sessionId)
}
}
222 changes: 222 additions & 0 deletions web-server/src/static/filetransfer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
const FILE_CHUNK_SIZE = 16384
const FILE_STREAM_SIZE = 32

const PACKET_ID = {
fileInfo: 0,
fileData: 1,
error: 9
}

const genIV = (i) => {
const iv = new Uint8Array(12)
const dataview = new DataView(iv.buffer)
dataview.setBigUint64(0, BigInt(i + 1))
return iv
}

const getJwkFromK = async (k) => {
const key = await crypto.subtle.importKey("jwk", {
alg: "A256GCM",
ext: true,
k,
kty: "oct",
key_ops: ["encrypt", "decrypt"]
}, { name: "AES-GCM" }, false, ["encrypt", "decrypt"])
return key
}

/**
* List containing all FileTransfer instances
*/
let activeFileTransfers = []

const newFileTransfer = (channel, key) => {
const fileTransfer = new FileTransfer(channel, key)
activeFileTransfers.push(fileTransfer)
return fileTransfer
}

const removeFileTransfer = (fileTransfer) => {
activeFileTransfers = activeFileTransfers.filter(o => o !== fileTransfer)
}

class FileTransfer {
packetIndex = 0

constructor(channel, key) {
this.channel = channel
this.key = key
}

sendAndEncryptPacket = async (packet) => {
const iv = genIV(packetIndex++) // TODO: Set this to random int

const encryptedPacket = await crypto.subtle.encrypt({
"name": "AES-GCM", "iv": iv
}, this.key, packet);

const encryptedPacketAndIV = new Uint8Array(encryptedPacket.byteLength + 12)
encryptedPacketAndIV.set(iv)
encryptedPacketAndIV.set(new Uint8Array(encryptedPacket), 12)
// console.log(encryptedPacketAndIV)
// console.log(encryptedPacket)
this.channel.send(encryptedPacketAndIV)
}

sendFile = async (file, cbProgress, cbFinished) => {
channel.addEventListener("message", async e => {
const data = JSON.parse(e.data)
if(data.type == "progress") {
cbProgress({ now: data.now, max: file.size })
if(data.now == file.size) {
cbFinished()
}
}
else if(data.type == "error") {
throw data.message
}
})

let offset = 0
let fr = new FileReader()
fr.onload = async e => {
const __data = fr.result

const packet = new Uint8Array(1 + 8 + __data.byteLength)
const packetDataView = new DataView(packet.buffer)
packetDataView.setInt8(0, PACKET_ID.fileData)
packetDataView.setBigUint64(1, BigInt(offset))
packet.set(new Uint8Array(__data), 1 + 8)

await sendAndEncryptPacket(channel, packet, key)
offset += __data.byteLength;
// cbProgress({ now: offset, max: file.size })
// console.log(offset + "/" + file.size)
if (offset < file.size) {
readSlice(offset);
}
}
fr.onerror = e => {
console.error("File reader error", e)
}
fr.onabort = e => {
console.log("File reader abort", e)
}
const readSlice = o => {
// console.log("readSlice", o)

if(channel.bufferedAmount > 5000000) {
//console.log("WAIT", channel.bufferedAmount)
return setTimeout(() => { readSlice(o) }, 1)
}
//console.log("READ", channel.bufferedAmount)

const slice = file.slice(offset, o + FILE_CHUNK_SIZE);
fr.readAsArrayBuffer(slice);
};
const fileInfo = {
name: file.name,
size: file.size,
type: file.type
}
const fileInfoStr = JSON.stringify(fileInfo)
const textEnc = new TextEncoder()
const fileInfoBytes = textEnc.encode(fileInfoStr)
console.log("Sending file info:", fileInfoStr, fileInfoBytes)

const packet = new Uint8Array(1 + fileInfoBytes.byteLength)
const packetDataView = new DataView(packet.buffer)
packetDataView.setInt8(0, PACKET_ID.fileInfo)
packet.set(fileInfoBytes, 1)

await this.sendAndEncryptPacket(packet)
readSlice(0)
}

recvFile = async (cbProgress, cbFinished) => {
let chunkMap = new Map()
let chunkIndex = -1
let writer = undefined
let bytesRecieved = 0
let fileInfo = undefined

const handleChunkMap = () => {
if (!writer) {
console.error("writer undefined")
return
}
if (writer.desiredSize == null) {
console.error("user canceled download")
channel.close()
return
}
if (!fileInfo) {
console.error("fileInfo undefined")
return
}
while (true) {
const data = chunkMap.get(chunkIndex + 1)
if (!data) break
chunkMap.delete(chunkIndex + 1)
chunkIndex++
writer.write(data)
}
if (bytesRecieved == fileInfo.size) {
console.log("Close writer")
writer.close()
}
}
this.channel.addEventListener("message", async e => {
const __data = e.data
const iv = new Uint8Array(__data.slice(0, 12))
const encryptedPacket = __data.slice(12)

const packet = new Uint8Array(await crypto.subtle.decrypt({
"name": "AES-GCM", "iv": iv
}, this.key, encryptedPacket));

const packetDataView = new DataView(packet.buffer)
const packetId = packetDataView.getInt8(0)

if (packetId == PACKET_ID.fileInfo) {
const data = packet.slice(1)

const textDec = new TextDecoder()
const fileInfoStr = textDec.decode(data)
const _fileInfo = JSON.parse(fileInfoStr)
fileInfo = _fileInfo
console.log("Got file info:", fileInfo)

const fileStream = streamSaver.createWriteStream(fileInfo.name, {
size: fileInfo.size
})
writer = fileStream.getWriter()
handleChunkMap() // if packet is received after all file data for some reason
}
else if (packetId == PACKET_ID.fileData) {
const offset = Number(packetDataView.getBigUint64(1))
const data = packet.slice(1 + 8)

const index = offset / FILE_CHUNK_SIZE
chunkMap.set(index, data)

bytesRecieved += data.byteLength
handleChunkMap()

let fileReceived = bytesRecieved == fileInfo.size

// TODO: This could be improved by taking into account the file size
if(index % 50 == 49 || fileReceived) {
this.channel.send(JSON.stringify({ type: "progress", now: bytesRecieved }))
}

cbProgress({ now: bytesRecieved, max: fileInfo.size })

if (fileReceived) {
console.log("File has been received!")
cbFinished()
}
}
})
}
}
11 changes: 7 additions & 4 deletions web-server/src/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@
<script src="/lib/qrcode.min.js"></script>

<script src="/persistance.js"></script>
<script src="/webrtc.js?v=1"></script>
<script defer src="/index.js?v=1"></script>
<script src="/webrtc.js"></script>
<script src="/filetransfer.js"></script>

<link rel="stylesheet" href="/index.css?v=1">
<script defer src="./ui.js"></script>
<script defer src="./index.js"></script>

<link rel="stylesheet" href="/index.css?v=2">
<link rel="stylesheet" href="/extra.css?v=1">

<link rel="manifest" href="/manifest.json">
Expand All @@ -46,7 +49,7 @@ <h5 id="alert-modal-title" class="modal-title">Error</h5>
</div>
</div>

<div id="add-contact-modal" class="modal" tabindex="-1">
<div id="add-contact-modal" class="modal fade" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
Expand Down
Loading

0 comments on commit 1cc96c5

Please sign in to comment.