Intro

(This blog contains potentially risky solutions which may violate a site’s EULA, viewer discretion advised)

Netflix famously said “love is sharing a password”..

No, actually, we can do better than that.

We can set up a reverse proxy for our friends to use, that reverse proxy will forward requests to the upstream site and return the responses. In the forwarding process, the reverse proxy will inject cookies to the request, so that the request will pass the authentication. By setting up something like this, our friends can use the same account as us, without knowing our password or even the need to log in. This can also solve several other problems, such as sites being blocked in our friend’s region / site having a limit on the number of concurrent sessions (one login will automatically log out all other browsers).

Implementation

I’ve been using this project: chimurai/http-proxy-middleware to build reverse proxies, it works great. Here is some sample code (please pardon the poor code quality as this is only a throw-away toy project):

import express from "express";
import fs from "fs";
import {createProxyMiddleware} from "http-proxy-middleware";
import zlib from "zlib";
import * as https from "https";

const config = JSON.parse(fs.readFileSync("./config.json", {encoding: "utf8"}));

const app = express();

process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";

app.get('/robots.txt', function (req, res) {
    res.type('text/plain');
    res.send("User-agent: *\nDisallow: /");
});

app.use(function (req, res, next) {
    delete req.headers["origin"];
    next();
})

const UpstreamSite = 'https://example.com/';

app.use(
    '/',
    createProxyMiddleware({
        target: UpstreamSite,
        changeOrigin: true,
        selfHandleResponse: true,

        headers: {
            cookie: config.cookies.map(x => `${x.name}=${x.value}`).join(";"),
            referer: UpstreamSite,
        },

        onProxyRes: async (proxyRes, req, res) => {
            if (res.headersSent) {
                return;
            }
            res.setHeader('cross-origin-embedder-policy', "require-corp");
            res.setHeader('access-control-allow-origin', "*");

            let buffer = Buffer.from('', 'utf8');
            
            // decompress proxy response
            const _proxyRes = decompress(proxyRes, proxyRes.headers['content-encoding']);

            const isEventStream = req.headers['accept'] === 'text/event-stream';
            if (isEventStream) {
                copyHeaders(proxyRes, res);
            }

            _proxyRes.on('data', (chunk) => {
                if (isEventStream) {
                    res.write(chunk);
                    return chunk;
                } else {
                    return (buffer = Buffer.concat([buffer, chunk]));
                }
            });

            _proxyRes.on('end', async () => {
                if (isEventStream) {
                    res.end();
                } else {
                    // copy original headers
                    copyHeaders(proxyRes, res);

                    if ((proxyRes.headers['content-type'] || '').indexOf("text/html") >= 0) {
                        const data = buffer.toString('utf8');
                        res.setHeader("content-security-policy", "script-src 'unsafe-inline' 'self'; connect-src 'self'; default-src 'self' 'unsafe-inline' data:");
                        res.end();
                        return;
                    }

                    const interceptedBuffer = Buffer.from(buffer);
                    res.setHeader('content-length', Buffer.byteLength(interceptedBuffer, 'utf8'));
                    res.write(interceptedBuffer);
                    res.end();
                }
            });
            _proxyRes.on('error', (error) => {
                res.end(`Error fetching proxied request: ${error.message}`);
            });
        },
    })
);

function copyHeaders(originalResponse, response) {
    response.statusCode = originalResponse.statusCode;
    response.statusMessage = originalResponse.statusMessage;
    if (response.setHeader) {
        let keys = Object.keys(originalResponse.headers);
        // ignore chunked, brotli, gzip, deflate headers
        keys = keys.filter((key) => !['content-encoding', 'transfer-encoding'].includes(key));
        keys.forEach((key) => {
            let value = originalResponse.headers[key];
            if (key === 'set-cookie') {
                // remove cookie domain
                value = Array.isArray(value) ? value : [value];
                value = value.map((x) => x.replace(/Domain=[^;]+?/i, ''));
            }
            response.setHeader(key, value);
        });
    } else {
        response.headers = originalResponse.headers;
    }
}

function decompress(proxyRes, contentEncoding) {
    let _proxyRes = proxyRes;
    let decompress;
    switch (contentEncoding) {
        case 'gzip':
            decompress = zlib.createGunzip();
            break;
        case 'br':
            decompress = zlib.createBrotliDecompress();
            break;
        case 'deflate':
            decompress = zlib.createInflate();
            break;
        default:
            break;
    }
    if (decompress) {
        _proxyRes.pipe(decompress);
        _proxyRes = decompress;
    }
    return _proxyRes;
}

const privateKey = fs.readFileSync(config.tlsPrivateKeyPath, 'utf8');
const certificate = fs.readFileSync(config.tlsCertPath, 'utf8');

const credentials = {
    key: privateKey,
    cert: certificate
};

const httpsServer = https.createServer(credentials, app);
const server = httpsServer.listen(8090);
server.setTimeout(600000);

Now, what you need to do is to change UpstreamSite, then put cookies in config.json (you can get cookies by inspecting the network requests in your browser). You may also specify TLS certificate in config.json.

If you run the script now, you will be able to access the upstream site (no need to log in) via IP:8090.

Chaining Another Proxy

If the upstream site is blocked from the server I’m running this script on, I will need to use another proxy to access the upstream site, how do I do this? It’s easy:

import HttpsProxyAgent from "https-proxy-agent";

const proxyAgent = new HttpsProxyAgent(config.proxy);

createProxyMiddleware({
    agent: proxyAgent,
    ...

Adding Authentication

We don’t want just anyone to use our reverse proxy, so we shall add some authentication. The simplest way to do this would be

app.get('/login', function (req, res) {
    res.cookie("password", req.query.password);
    return res.redirect("/");
});

app.use(function (req, res, next) {
    if (req.path === '/login') {
        next();
        return;
    }
    if (!req.headers.cookie || req.headers.cookie.indexOf(config.password) < 0) {
        res.status(401);
        res.end("");
        return;
    }

    next();
})

Blocking Some Features

We don’t want our friend to click “Logout” on the site, as it will impact other users. We can block requests by

app.all("/signout", function (req, res) {
    res.status(401);
    res.end("");
    return;
});

Patching the Request

According to http-proxy-middleware’s documentation, to patch the request, we will need to use an express middleware.

app.use(function (req, res, next) {
    delete req.headers["origin"];
    ...

Patching the Response

We may want to patch the site to hide some content, or limit some functionalities, or change the site for whatever reasons..

In _proxyRes.on('end', async () => {, we can add code to do this

if ((proxyRes.headers['content-type'] || '').indexOf("application/javascript") >= 0) {
    const data = buffer.toString('utf8');
    // do something to `data`
    res.write(data);
    res.end();
    return;
}

Final Words of Caution

  1. Make sure you analyse the site you are trying to reverse proxy, and make sure you are not breaking any laws. I am not responsible for any damages caused by this script.
  2. Try to do this stealthily. Open DevTools on your site powered by the reverse-proxy, make sure no request is sent directly to the upstream site (only the reverse-proxy can send sanitized requests to the upstream site, or else the upstream site may detect something fishy) (How to do it? Patch out the part of code that sends this kind of requests using the method in Patching the response, or impose content security policy). Also, analyse all the data the reverse proxy is sending to the upstream server, and make sure that it doesn’t look suspicious (especially with regard to the Origin, Referer headers, etc.).

A Wild Idea

I always thought it would be cool if I could share my Private Tracker site accounts with my friends using this method.. I could hijack the “download” button, and make it directly send the torrent file to the bittorrent client (e.g. qbittorrent-nox) running on the server. When it has finished downloading, they could use FTP to fetch the files, and the bittorrent client will keep seeding.. (I know it is a serious violation of the rules of Private Tracker sites, I never carried it out, it’s just a thought..)