用NodeJS实现HTTP/HTTPS代理

用NodeJS实现HTTP/HTTPS代理

身在天朝,难免会用到代理的时候。 比如在学校内网用代理免费上外网,在墙内用代理上404网站等。 <br/> <br/>现在使用的代理大部分为HTTP和Socket代理。 Socket代理更底层,需要本地解析域名,而HTTP代理则是基于HTTP协议之上的,不需要本地解析域名。下面我讲讲HTTP(S)代理的设计思路以及NodeJS代码实现。 <br/> <br/><strong>HTTP协议</strong> <br/> <br/>HTTP协议简单说来就是浏览器把一串字符串发送到目标服务器,然后把目标服务器返回回来的一串字符串显示给用户。 <br/> <br/>浏览器发送的这串字符主要分为两个部分,一部分是头,里面包含目标服务器域名,当前请求的文件路径等信息。另一部分是正文,一般的GET请求没有正文。 <br/> <br/>服务器返回来的字符串也分为头和正文。 <br/> <br/><strong>HTTP代理原理</strong> <br/> <br/>HTTP代理需要做的事情就是接收浏览器发来的请求字符串,再从请求字符串的头部分找出浏览器请求的目标主机,然后直接把这串请求字符串发给目标主机,再把目标主机返回的数据发给浏览器。 “什么?就这么简单?” “呃。。是啊,但这还没完。。” <br/> <br/>现代浏览器一般都是默认采用HTTP/1.1版本,并且默认会发送Connection: keep-alive请求。 这些信息是写在请求的头部的,意思是通知目标服务器采用keep-alive技术继续处理后续的请求。 但是我们做的代理程序要想支持keep-alive是比较麻烦的。所以干脆就把这个篡改成Connection: close。 这样就可以保证浏览器请求的每个文件都会单独发送一个HTTP请求。 <br/> <br/><strong>下面是NodeJS代码实现</strong> <br/><pre escaped=“true” lang=“javascript”>var net = require(‘net’); <br/>var local_port = 8893; <br/> <br/>//在本地创建一个server监听本地local_port端口 <br/>net.createServer(function (client) <br/>{ <br/> <br/> //首先监听浏览器的数据发送事件,直到收到的数据包含完整的http请求头 <br/> var buffer = new Buffer(0); <br/> client.on(‘data’,function(data) <br/> { <br/> buffer = buffer_add(buffer,data); <br/> if (buffer_find_body(buffer) == -1) return; <br/> var req = parse_request(buffer); <br/> if (req === false) return; <br/> client.removeAllListeners(‘data’); <br/> relay_connection(req); <br/> }); <br/> <br/> //从http请求头部取得请求信息后,继续监听浏览器发送数据,同时连接目标服务器,并把目标服务器的数据传给浏览器 <br/> function relay_connection(req) <br/> { <br/> console.log(req.method+’ ‘+req.host+’:’+req.port); <br/> <br/> //如果请求不是CONNECT方法(GET, POST),那么替换掉头部的一些东西 <br/> if (req.method != ‘CONNECT’) <br/> { <br/> //先从buffer中取出头部 <br/> var _body_pos = buffer_find_body(buffer); <br/> if (_body_pos < 0) _body_pos = buffer.length; <br/> var header = buffer.slice(0,_body_pos).toString(‘utf8’); <br/> //替换connection头 <br/> header = header.replace(/(proxy-)?connection:.+\r\n/ig,’’) <br/> .replace(/Keep-Alive:.+\r\n/i,’’) <br/> .replace("\r\n",’\r\nConnection: close\r\n’); <br/> //替换网址格式(去掉域名部分) <br/> if (req.httpVersion == ‘1.1’) <br/> { <br/> var url = req.path.replace(/http://[^/]+/,’’); <br/> if (url.path != url) header = header.replace(req.path,url); <br/> } <br/> buffer = buffer_add(new Buffer(header,‘utf8’),buffer.slice(_body_pos)); <br/> } <br/> <br/> //建立到目标服务器的连接 <br/> var server = net.createConnection(req.port,req.host); <br/> //交换服务器与浏览器的数据 <br/> client.on(“data”, function(data){ server.write(data); }); <br/> server.on(“data”, function(data){ client.write(data); }); <br/> <br/> if (req.method == ‘CONNECT’) <br/> client.write(new Buffer(“HTTP/1.1 200 Connection established\r\nConnection: close\r\n\r\n”)); <br/> else <br/> server.write(buffer); <br/> } <br/>}).listen(local_port); <br/> <br/>console.log(‘Proxy server running at localhost:’+local_port); <br/> <br/>//处理各种错误 <br/>process.on(‘uncaughtException’, function(err) <br/>{ <br/> console.log("\nError!!!"); <br/> console.log(err); <br/>}); <br/> <br/>/** <br/>* 从请求头部取得请求详细信息 <br/>* 如果是 CONNECT 方法,那么会返回 { method,host,port,httpVersion} <br/>* 如果是 GET/POST 方法,那么返回 { metod,host,port,path,httpVersion} <br/>/ <br/>function parse_request(buffer) <br/>{ <br/> var s = buffer.toString(‘utf8’); <br/> var method = s.split(’\n’)[0].match(/^([A-Z]+)\s/)[1]; <br/> if (method == ‘CONNECT’) <br/> { <br/> var arr = s.match(/^([A-Z]+)\s([^:\s]+):(\d+)\sHTTP/(\d.\d)/); <br/> if (arr && arr[1] && arr[2] && arr[3] && arr[4]) <br/> return { method: arr[1], host:arr[2], port:arr[3],httpVersion:arr[4] }; <br/> } <br/> else <br/> { <br/> var arr = s.match(/^([A-Z]+)\s([^\s]+)\sHTTP/(\d.\d)/); <br/> if (arr && arr[1] && arr[2] && arr[3]) <br/> { <br/> var host = s.match(/Host:\s+([^\n\s\r]+)/)[1]; <br/> if (host) <br/> { <br/> var _p = host.split(’:’,2); <br/> return { method: arr[1], host:_p[0], port:_p[1]?_p[1]:80, path: arr[2],httpVersion:arr[3] }; <br/> } <br/> } <br/> } <br/> return false; <br/>} <br/> <br/>/* <br/>* 两个buffer对象加起来 <br/>/ <br/>function buffer_add(buf1,buf2) <br/>{ <br/> var re = new Buffer(buf1.length + buf2.length); <br/> buf1.copy(re); <br/> buf2.copy(re,buf1.length); <br/> return re; <br/>} <br/> <br/>/* <br/>* 从缓存中找到头部结束标记("\r\n\r\n")的位置 <br/>*/ <br/>function buffer_find_body(b) <br/>{ <br/> for(var i=0,len=b.length-3;i < len;i++) <br/> { <br/> if (b[i] == 0x0d && b[i+1] == 0x0a && b[i+2] == 0x0d && b[i+3] == 0x0a) <br/> { <br/> return i+4; <br/> } <br/> } <br/> return -1; <br/>}</pre> <br/>另外,可以用 “nohup node some.js > /dev/null &” 命令让nodejs程序在后台运行。


8 回复

用NodeJS实现HTTP/HTTPS代理

背景

身在中国,有时需要使用代理来访问互联网。比如在学校内网中使用代理免费上外网,或者在受限网络环境下访问特定网站。

目前常用的代理类型主要是HTTP和Socket代理。Socket代理更加底层,需要本地解析域名;而HTTP代理则基于HTTP协议之上,不需要本地解析域名。本文将介绍HTTP(S)代理的设计思路及如何用NodeJS实现HTTP/HTTPS代理。

HTTP协议

HTTP协议的核心思想是浏览器将一串字符串发送到目标服务器,服务器再将响应返回给浏览器。请求字符串由两部分组成:头部和正文。头部包含了目标服务器域名、文件路径等信息,正文则是实际的数据。

HTTP代理原理

HTTP代理的主要任务是接收来自浏览器的请求字符串,解析出目标主机信息,然后将请求字符串转发给目标主机,并将目标主机的响应返回给浏览器。

需要注意的是,现代浏览器通常默认采用HTTP/1.1版本,并发送Connection: keep-alive请求,这意味着目标服务器会保持连接以处理后续请求。为了简化我们的代理程序,我们将这个请求头修改为Connection: close,这样可以确保每次请求都是独立的。

NodeJS代码实现

以下是一个简单的NodeJS实现HTTP代理的示例:

const net = require('net');

const localPort = 8893;

// 创建一个监听本地端口的server
net.createServer((client) => {
    // 监听客户端数据发送事件
    let buffer = new Buffer(0);

    client.on('data', (data) => {
        buffer = buffer_add(buffer, data);
        if (buffer_find_body(buffer) === -1) return;

        const req = parse_request(buffer);
        if (!req) return;

        client.removeAllListeners('data');
        relay_connection(req);
    });

    // 处理请求并转发到目标服务器
    function relay_connection(req) {
        console.log(`${req.method} ${req.host}:${req.port}`);

        // 对非CONNECT方法的请求进行处理
        if (req.method !== 'CONNECT') {
            // 取出头部
            const bodyPos = buffer_find_body(buffer);
            if (bodyPos < 0) bodyPos = buffer.length;
            let header = buffer.slice(0, bodyPos).toString('utf8');

            // 替换连接头
            header = header.replace(/(proxy-)?connection:.+\r\n/ig, '')
                            .replace(/Keep-Alive:.+\r\n/i, '')
                            .replace('\r\n', '\r\nConnection: close\r\n');

            // 替换网址格式
            if (req.httpVersion === '1.1') {
                const url = req.path.replace(/http:\/\/[^\/]+\//, '');
                if (url !== req.path) header = header.replace(req.path, url);
            }
            buffer = buffer_add(new Buffer(header, 'utf8'), buffer.slice(bodyPos));
        }

        // 建立到目标服务器的连接
        const server = net.createConnection(req.port, req.host);

        // 交换数据
        client.on("data", (data) => server.write(data));
        server.on("data", (data) => client.write(data));

        if (req.method === 'CONNECT') {
            client.write(new Buffer("HTTP/1.1 200 Connection established\r\nConnection: close\r\n\r\n"));
        } else {
            server.write(buffer);
        }
    }
}).listen(localPort);

console.log(`Proxy server running at localhost:${localPort}`);

// 错误处理
process.on('uncaughtException', (err) => {
    console.log("\nError!!!");
    console.log(err);
});

/**
 * 解析请求头部,获取请求详细信息
 */
function parse_request(buffer) {
    const s = buffer.toString('utf8');
    const method = s.split('\n')[0].match(/^([A-Z]+)\s/)[1];

    if (method === 'CONNECT') {
        const arr = s.match(/^([A-Z]+)\s([^:\s]+):(\d+)\sHTTP\/(\d.\d)/);
        if (arr && arr[1] && arr[2] && arr[3] && arr[4]) {
            return { method: arr[1], host: arr[2], port: arr[3], httpVersion: arr[4] };
        }
    } else {
        const arr = s.match(/^([A-Z]+)\s([^\s]+)\sHTTP\/(\d.\d)/);
        if (arr && arr[1] && arr[2] && arr[3]) {
            const host = s.match(/Host:\s+([^\n\s\r]+)/)[1];
            if (host) {
                const p = host.split(':', 2);
                return { method: arr[1], host: p[0], port: p[1] ? p[1] : 80, path: arr[2], httpVersion: arr[3] };
            }
        }
    }
    return false;
}

/**
 * 将两个Buffer对象合并
 */
function buffer_add(buf1, buf2) {
    const re = new Buffer(buf1.length + buf2.length);
    buf1.copy(re);
    buf2.copy(re, buf1.length);
    return re;
}

/**
 * 查找头部结束标记的位置
 */
function buffer_find_body(b) {
    for (let i = 0, len = b.length - 3; i < len; i++) {
        if (b[i] === 0x0d && b[i + 1] === 0x0a && b[i + 2] === 0x0d && b[i + 3] === 0x0a) {
            return i + 4;
        }
    }
    return -1;
}

以上代码展示了如何通过NodeJS实现一个简单的HTTP代理服务器。该代理服务器能够接收来自浏览器的请求,解析请求头并替换部分内容,然后转发请求到目标服务器,并将响应返回给浏览器。希望这段代码对你有所帮助!


可以试一试 http://nowall.be/here ,这也是nodejs开发的

测试好用^_^

应该不行吧,这个貌似没有做加密的,所以我把这个代码部署到us服务器上还是不能访问404页面的(facebook)

话说,这个能代理https? 不行吧。。。。

如访问的是超大文件,要等到node.js 缓冲整个文件后,向客户端转发么?

为何日志显示没有post请求?

为了实现一个简单的 HTTP 和 HTTPS 代理,我们可以使用 Node.js 的 net 模块来创建 TCP 服务器,以便处理客户端的连接,并将这些连接转发到目标服务器。下面是一个基本的实现,包括如何处理 HTTP 请求和响应。

示例代码

const net = require('net');
const http = require('http');

const LOCAL_PORT = 8893;

net.createServer((client) => {
    // 设置客户端的 'data' 事件处理器
    let buffer = new Buffer(0);
    client.on('data', (data) => {
        buffer = Buffer.concat([buffer, data]);
        if (buffer.indexOf('\r\n\r\n') === -1) return; // 等待完整的 HTTP 头部
        
        const request = parseRequest(buffer.toString());
        if (!request) return;

        client.removeAllListeners('data'); // 移除所有 'data' 事件处理器

        relayConnection(client, request);
    });
}).listen(LOCAL_PORT);

console.log(`Proxy server running at localhost:${LOCAL_PORT}`);

function relayConnection(client, request) {
    console.log(`${request.method} ${request.host}:${request.port}`);
    
    // 如果不是 CONNECT 方法,则替换头部中的某些字段
    if (request.method !== 'CONNECT') {
        request.headers['connection'] = 'close';
        if (request.httpVersion === '1.1') {
            request.url = request.url.replace(/^http:\/\/[^\/]+\//, '/');
        }
        client.write(formatRequest(request));
    }

    // 创建到目标服务器的连接
    const server = net.connect({ port: request.port, host: request.host }, () => {
        if (request.method === 'CONNECT') {
            client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
        } else {
            server.write(buffer.slice(buffer.indexOf('\r\n\r\n') + 4));
        }
    });

    // 交换数据
    client.pipe(server);
    server.pipe(client);
}

function parseRequest(requestStr) {
    const lines = requestStr.split('\r\n');
    const firstLine = lines.shift().split(' ');
    const method = firstLine[0];
    const url = firstLine[1];
    const version = firstLine[2];

    if (method === 'CONNECT') {
        const [host, port] = url.split(':');
        return { method, host, port, httpVersion: version };
    }

    const headers = {};
    for (let line of lines) {
        if (!line) break;
        const [key, value] = line.split(': ');
        headers[key.toLowerCase()] = value;
    }

    const host = headers['host'];
    const [hostName, port] = host.split(':').concat(['80']);
    return { method, host: hostName, port: parseInt(port), path: url, httpVersion: version, headers };
}

function formatRequest(request) {
    const headerLines = [
        `${request.method} ${request.url} ${request.httpVersion}`,
        ...Object.keys(request.headers).map(key => `${key}: ${request.headers[key]}`)
    ];
    return headerLines.join('\r\n') + '\r\n\r\n';
}

解释

  1. TCP 服务器:使用 net.createServer() 创建一个 TCP 服务器,监听指定的端口(例如 8893)。
  2. 缓冲区管理:使用 Buffer 来累积客户端发送的数据,直到接收到完整的 HTTP 请求头部。
  3. 解析请求parseRequest() 函数解析 HTTP 请求字符串,提取请求的方法、URL、版本号和头部信息。
  4. 建立连接relayConnection() 函数负责建立到目标服务器的连接,并设置管道以转发数据。
  5. 替换头部:对于非 CONNECT 方法的请求,替换头部中的 Connection 字段为 close,以确保每个请求都是独立的。
  6. 数据转发:使用 client.pipe(server)server.pipe(client) 实现双向数据流。

这个示例提供了一个基本的 HTTP 代理实现,可以进一步扩展以支持更多功能,例如 HTTPS 支持、身份验证等。

回到顶部