Proxying and tunneling
Sometimes, it is useful to provide a means for one server to function as a proxy, or broker, for other servers. This would allow one server to distribute a load to other servers, for example. Another use would be to provide access to a secured server to users who are unable to connect to that server directly. It is also common to have one server answering for more than one URL—using a proxy, that one server can forward requests to the right recipient.
Because Node has a consistent streams interface throughout its network interfaces, we can build a simple HTTP proxy in just a few lines of code. For example, the following program will set up an HTTP server on port 8080 which will respond to any request by fetching the front page of a website and piping that page back to the client:
const http = require('http');
const server = new http.Server();
server.on("request", (request, socket) => {
console.log(request.url);
http.request({
host: 'www.example.org',
method: 'GET',
path: "/",
port: 80
}, response => response.pipe(socket))
.end();
});
server.listen(8080, () => console.log('Proxy server listening on localhost:8080'));
Go ahead and start this server, and connect to it. Once this server receives the client socket, it is free to push content from any readable stream back to the client, and here, the result of GET of www.example.org is streamed. One can easily see how an external content server managing a caching layer for your application might become a proxy endpoint, for example.
Using similar ideas, we can create a tunneling service, using Node's native CONNECT support. Tunneling involves using a proxy server as an intermediary to communicate with a remote server on behalf of a client. Once our proxy server connects to a remote server, it is able to pass messages back and forth between that server and a client. This is advantageous when a direct connection between a client and a remote server is not possible, or not desired.
First, we'll set up a proxy server responding to HTTP CONNECT requests, then make a CONNECT request to that server. The proxy receives our client's Request object, the client's socket itself, and the head (the first packet) of the tunneling stream:
const http = require('http');
const net = require('net');
const url = require('url');
const proxy = new http.Server();
proxy.on('connect', (request, clientSocket, head) => {
let reqData = url.parse(`http://${request.url}`);
let remoteSocket = net.connect(reqData.port, reqData.hostname, () => {
clientSocket.write('HTTP/1.1 200 \r\n\r\n');
remoteSocket.write(head);
remoteSocket.pipe(clientSocket);
clientSocket.pipe(remoteSocket);
});
}).listen(8080);
let request = http.request({
port: 8080,
hostname: 'localhost',
method: 'CONNECT',
path: 'www.example.org:80'
});
request.end();
request.on('connect', (res, socket, head) => {
socket.setEncoding("utf8");
socket.write('GET / HTTP/1.1\r\nHost: www.example.org:80\r\nConnection: close\r\n\r\n');
socket.on('readable', () => {
console.log(socket.read());
});
socket.on('end', () => {
proxy.close();
});
});
Once we make a request to our local tunneling server running on port 8080 it will set up a remote socket connection to our destination and maintain this "bridge" between the remote socket and the (local) client socket. The remote connection of course only sees our tunneling server, and in this way clients can connect in a sense anonymously to remote services (which isn't always a shady practice!).