pair-relayer
v1.0.0
Published
Node.js server that listens for incoming connections and forwards data between them
Downloads
1
Readme
pair-relayer
Simple (and insecure) way of relaying data between two machines connected to this server.
This server will admit multiple connections but will get pair of sockets and pipe the data from each other.
The code for this server has been completely generated by ChatGPT3.5 and it's prompts are registered in this readme file.
Motivations
I just wanted to play Wacky Wheels. DoxBOX, the emulator used, emulates also the old serial connection into TCP/IP connection, without any type of protocol, just raw data going through raw (insecure) internet pipes.
In order to play, I needed a relay server that connects two machines and start forwarding each other data. At the same time, we are living in the boom of AI generative tools and, despite of knowing what to do, I asked ChatGPT 3.5 how to do it.
So, this is the result of an AI helping me to get the software that I wanted, and the steps that I did to make it happen.
The prompts
Step 1: The main code
I asked to generate the server, I wanted to be sure the server can support more than 2 connections (we are a bunch of friends, and want to do a championship :)), here is the prompt:
Write a nodejs program that listens in port 3000 for incoming connections.
It will group connections in pairs, forwarding each other data and keep a list of open pairs of sockets.
And the answer:
const net = require('net');
const socketPairs = [];
const server = net.createServer((socket) => {
if (socketPairs.length % 2 === 0) {
// Create a new socket pair
const newPair = [socket];
socketPairs.push(newPair);
} else {
// Forward data between the last pair and the new socket
const lastPair = socketPairs[socketPairs.length - 1];
lastPair.push(socket);
forwardData(lastPair);
}
// Handle socket events
socket.on('data', (data) => {
const pair = getPair(socket);
const otherSocket = getOtherSocket(pair, socket);
otherSocket.write(data);
});
socket.on('end', () => {
const pair = getPair(socket);
if (pair) {
// Remove the socket pair
socketPairs.splice(socketPairs.indexOf(pair), 1);
}
});
socket.on('error', (err) => {
console.error(`Socket error: ${err}`);
});
});
function getPair(socket) {
return socketPairs.find((pair) => pair.includes(socket));
}
function getOtherSocket(pair, socket) {
return pair.find((s) => s !== socket);
}
function forwardData(pair) {
const [socket1, socket2] = pair;
socket1.pipe(socket2);
socket2.pipe(socket1);
}
server.listen(3000, () => {
console.log('Server listening on port 3000');
});
Almost spot on! On the first try! Or at least that's what it looks like.
Step 2: Refine
I observed that on the end
event we are just removing from memory the pair os sockets and I asked to be sure we disconnect both clients before removing from memory.
Here is prompt:
The program seems correct, to ensure that is more accurate, it does remove the socket pair when one of the ends finish the connection, can you ensure that you close the connection on the other socket before removing from memory?
And here is the answer:
const net = require('net');
const socketPairs = [];
const server = net.createServer((socket) => {
if (socketPairs.length % 2 === 0) {
// Create a new socket pair
const newPair = [socket];
socketPairs.push(newPair);
} else {
// Forward data between the last pair and the new socket
const lastPair = socketPairs[socketPairs.length - 1];
lastPair.push(socket);
forwardData(lastPair);
}
// Handle socket events
socket.on('data', (data) => {
const pair = getPair(socket);
const otherSocket = getOtherSocket(pair, socket);
otherSocket.write(data);
});
socket.on('end', () => {
const pair = getPair(socket);
if (pair) {
// Close the other socket before removing the pair
const otherSocket = getOtherSocket(pair, socket);
otherSocket.end();
// Remove the socket pair
socketPairs.splice(socketPairs.indexOf(pair), 1);
}
});
socket.on('error', (err) => {
console.error(`Socket error: ${err}`);
});
});
function getPair(socket) {
return socketPairs.find((pair) => pair.includes(socket));
}
function getOtherSocket(pair, socket) {
return pair.find((s) => s !== socket);
}
function forwardData(pair) {
const [socket1, socket2] = pair;
socket1.pipe(socket2);
socket2.pipe(socket1);
}
server.listen(3000, () => {
console.log('Server listening on port 3000');
});
Step 3: Checking that we are doing the right things
Now I decided to question certain decisions to understand that our server does wht I want:
Can we enter in a loop because of the 'end' event in both sockets?
With the following correct answer:
No, we won't enter into a loop because of the end event being emitted on both sockets. The reason is that when one of the sockets emits the end event, the corresponding pair is removed from the socketPairs array and the other socket is closed using the end method. This means that when the other socket emits the end event, it will not find the corresponding pair in the socketPairs array and nothing will happen.
To clarify, in this implementation, the end event is only used to close the socket and remove the corresponding pair from the socketPairs array. It is not used for any other purpose that would cause a loop or other unexpected behavior.
During my code review and testing of the solution found 3 bugs:
- Deciding when do we have a new pair fails since it checks by length %2, so when we get to the 1 pair, we always hit the same
else
code branch. - We pipe the sockets and added the
data
event, ending with duplicate information in both ends. - During the
end
event, theotherSocket
variable might be null while disconnecting, producing a fatal error while calling a method in a null object.
Here are the 3 prompts that I did to fix them:
The code, once you create the first pair of sockets, it nevers create more pairs as the socketsPair length'%' operation will be always 1.
As we are forwarding sockets, we don't need to listen to the 'data' event on sockets.
When you are handling a socket closing on the 'end' event, check that otherSocket is null before trying to execute the 'end' method
And the final version of the code, that is the initial commit in this repo:
const net = require('net');
const socketPairs = [];
const server = net.createServer((socket) => {
if (socketPairs.length === 0 || socketPairs[socketPairs.length - 1].length === 2) {
// Create a new socket pair
const newPair = [socket];
socketPairs.push(newPair);
} else {
// Forward data between the last pair and the new socket
const lastPair = socketPairs[socketPairs.length - 1];
lastPair.push(socket);
forwardData(lastPair);
}
socket.on('end', () => {
const pair = getPair(socket);
if (pair) {
// Close the other socket before removing the pair
const otherSocket = getOtherSocket(pair, socket);
if (otherSocket) {
otherSocket.end();
}
// Remove the socket pair
socketPairs.splice(socketPairs.indexOf(pair), 1);
}
});
socket.on('error', (err) => {
console.error(`Socket error: ${err}`);
});
});
function getPair(socket) {
return socketPairs.find((pair) => pair.includes(socket));
}
function getOtherSocket(pair, socket) {
return pair.find((s) => s !== socket);
}
function forwardData(pair) {
const [socket1, socket2] = pair;
socket1.pipe(socket2);
socket2.pipe(socket1);
}
server.listen(3000, () => {
console.log('Server listening on port 3000');
});
Conclusions
- It was fun to use.
- It definitely saved me 80% of the time, in a task that I knew how to do.
- It failed in their first intent.
- I had to do a code review (and testing) to provide instructions to fix the problems.
- Those intructions provided a valid solution.
From now on, I'll continue changing this code through prompts, that will be added to the different commit messages.