一、先说说这玩意儿到底是啥

想象一下快递中转站:你买的东西从商家发出,先到中转站,再送到你手里。正常情况下,中转站只是转手。但如果这个中转站"不正经"——它不仅能偷看你的包裹,还能把里面的说明书换成别的内容,甚至往包裹里塞小广告——这就是这个工具干的事。

这是一个用C语言写的底层网络代理程序。它站在你的浏览器和目标网站之间,所有进出的流量都要经过它手。它的本事主要有三块:

第一块:正经代理。 你访问啥网站,它帮你转发请求,再把网站返回的内容传回给你。支持普通的HTTP,也支持加密的HTTPS。

第二块:内容篡改。 这是它的核心花活。它能在网页传输过程中,按照你设定的规则修改网页内容。比如把所有<h1>标题后面插一段广告,或者把网页里所有的链接都替换成某个视频地址(对,就是那个著名的Rickroll)。

第三块:SSL中间人。 对于HTTPS加密流量,它自带了一套SSL证书体系。浏览器以为在和真网站加密通信,实际上是在和这个代理加密通信;代理解密内容改完以后,再和真网站加密通信。这样一来,加密流量也能被"动手脚"。

项目还自带了两个"恶作剧模式":一个是-gravity,会自动往网页里注入一段JavaScript,让网页上的元素像有了重力一样往下掉;另一个是-rickroll,会把网页里所有的超链接都换成Rick Astley那首《Never Gonna Give You Up》的YouTube链接。

二、这背后涉及了哪些技术领域

别看代码量不大,它横跨了好几个技术方向:

领域 具体涉及内容
网络编程 Socket编程、TCP连接管理、端口监听、非阻塞/超时处理
HTTP协议 请求/响应格式解析、Header处理、Content-Length计算、Transfer-Encoding、Content-Type判断
SSL/TLS安全 OpenSSL库使用、证书链、SSL握手(双向)、X.509证书伪造/自签名
数据压缩 Zlib/gzip解压与压缩(处理网页传输中的压缩内容)
正则表达式 POSIX regex编译与匹配、HTML标签/属性的特殊正则构造
进程管理 Fork多进程模型、僵尸进程处理(SIGCHLD信号)、父子进程资源回收
字符串处理 C语言内存管理、字符串替换、缓冲区动态扩容

三、整体设计思路:简单直接,不整花活

在这里插入代码片

这个程序的设计哲学就一个字:。没有线程池、没有事件循环、没有复杂的异步框架,就是最经典的Unix网络编程三板斧:

1. 多进程模型:来一个连,fork一个娃

主进程只管一件事:在端口上监听(默认9999)。每当有浏览器连上来,accept之后立刻fork()一个子进程。子进程负责处理这个连接的全部生命周期——从读请求、连目标网站、改内容、到返回响应。处理完就退出。主进程继续accept下一个。

while(1){
    newfd = accept(sockfd, ...);
    if (fork() == 0){       // 子进程
        close(sockfd);      // 子进程不需要监听套接字
        proxyHttp(newfd, userEditPage);
        close(newfd);
        exit(0);
    } else {                // 父进程
        close(newfd);       // 父进程不需要连接套接字
    }
}

这种设计简单粗暴,但非常符合Unix哲学:一个连接一个进程,崩溃也不影响主进程。代码里还注册了SIGCHLD信号处理函数来回收僵尸进程,防止子进程退出后变成"孤魂野鬼"占用系统资源。

2. 模块化回调:解析和篡改分离

程序把"网络转发"和"内容篡改"解耦了。proxyHttp函数只管网络层面的转发,当它发现返回的是HTML内容时,调用一个回调函数editCallback(也就是userEditPage)来处理内容。改完后再继续发送。

这种设计的好处是:如果你想加新的恶作剧功能,不需要动网络转发的代码,只需要写一个新的回调函数塞进去就行。

3. 命令行驱动:所有配置靠参数

程序没有配置文件,所有功能都通过命令行参数开启。参数解析用了一个简单的循环匹配:

while( (o=parseArgs(argc, argv, &cur)) != -1){
    switch(o){
        case CL_REGEX:  // 设置正则
        case CL_STRING: // 设置插入字符串
        case CL_FILES:  // 设置插入文件
        case CL_AFTER:  // 设置插入位置
        // ... 等等
    }
}

四、核心流程图解:一个请求是怎么被"动手脚"的

整体数据流

  用户请求
     │
     ▼
┌─────────┐    ┌──────────┐    ┌──────────┐
│  socket │───►│ HTTP解析  │───►│ 提取Host │
│  监听   │    │ 请求头   │    │ 和Path   │
└─────────┘    └──────────┘    └────┬─────┘
                                    │
                                    ▼
┌─────────┐    ┌──────────┐    ┌──────────┐
│  socket │◄───│ HTTP响应  │◄───│ 连接目标 │
│  返回   │    │ 组装返回  │    │ 服务器   │
└─────────┘    └──────────┘    └────┬─────┘
                                    │
                    ┌───────────────┘
                    │
                    ▼
            ┌──────────────┐
            │  判断是HTML?  │──否──► 直接转发
            └──────┬───────┘
                   │是
                   ▼
            ┌──────────────┐
            │ gzip解压      │
            │ 正则匹配替换   │
            │ 更新Content-Length
            └──────────────┘

对于HTTPS流量的特殊处理

浏览器访问 https://example.com 时,流程会更复杂一些:

浏览器访问 https://example.com

    │
    ▼
┌─────────────────────────────────────┐
│ 1. 浏览器 ──CONNECT──► 代理         │
│    (以为代理是目标服务器)            │
│ 2. 代理 ──SSL握手──► 浏览器         │
│    (出示伪造证书,证书由自带CA签名)    │
└─────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────┐
│ 3. 代理 ──TCP连接──► 真实服务器     │
│ 4. 代理 ──SSL握手──► 真实服务器     │
│    (代理此时是"客户端"身份)         │
└─────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────┐
│ 5. 真实服务器返回加密网页             │
│ 6. 代理解密 ──篡改内容 ──重新加密     │
│ 7. 浏览器收到"合法"的加密响应         │
│    (其实内容已被偷偷修改)            │
└─────────────────────────────────────┘

这里的关键是代理需要两套SSL身份

  • 面对浏览器时,它是"服务器",需要用自己的CA证书签发一个伪造的example.com证书
  • 面对真实网站时,它是"客户端",验证真实网站的证书

代码里用SSLWrap(&req, SSL_ACCEPT | HTTP_REQ)SSLWrap(&res, SSL_CONNECT | HTTP_RES)分别处理这两侧。

五、代码里的几个关键"机关"

1. 内容篡改的核心:replaceFunc

这是整个程序最精妙的函数之一。它不负责真正的替换,而是负责告诉程序在哪里切一刀

int replaceFunc(const char* string, Range* r){
    static int limit = -2;
    if (limit == -2) limit = Prox.options.count; // 初始化匹配次数限制
    
    // 用正则找到匹配位置
    if (Prox.match(string, r, Prox.regex) != 0) return -1;
    
    // 根据用户指定的位置(before/after/replace/append/prepend)调整Range
    switch (Prox.options.position){
        case CL_BEFORE:   // 在匹配内容之前插入
            r->end = r->start;
            break;
        case CL_AFTER:    // 在匹配内容之后插入
            r->start = r->end;
            break;
        case CL_REPLACE:  // 替换匹配内容本身
            break;
        case CL_APPEND:   // 在匹配标签结束前插入
            r->start = (r->end -= Prox.options.offset);
            break;
        case CL_PREPEND:  // 在匹配标签开始后插入
            // 找到结束标记的位置...
            break;
    }
    // 返回下次搜索的偏移量
    return offset;
}

这个函数返回的是一个Range(起始位置和结束位置),真正的字符串替换由replaceAllinsertFiles完成。这种"找位置+切分"的设计让同一段逻辑能同时支持"插入字符串"和"插入文件"两种操作。

2. HTML特殊匹配:不用写正则,直接指定标签

程序很贴心地提供了-matchtag-matchattr参数。你不需要写复杂的正则去匹配<a href="...">,直接说"我要匹配所有href属性"就行。

代码里会根据你的输入,自动编译出对应的正则:

  • 匹配HTML标签:compileRegexTag(tag),比如匹配<h1>...</h1>
  • 匹配HTML属性:compileRegexAttr(attr, tag),比如匹配href="..."

3. HTTP头的"外科手术"

程序不仅能改网页内容,还能改HTTP头。支持三种操作:

  • --add-headers:新增头,比如加个Set-Cookie
  • --replace-headers:替换头,比如把Last-Modified改掉
  • --block-headers:删除头,比如干掉X-XSS-ProtectionContent-Security-Policy

这最后一条很有意思——删掉安全相关的HTTP头,本质上是在"削弱"目标网站的安全防护,为后续的脚本注入铺路。

4. Gzip的"剥洋葱"处理

现代网站为了省流量,响应内容通常是gzip压缩的。程序的处理很直接:

if (strstr(H->data, "gzip") != (char*)0){
    decodeGzip(&res.store->content, &res.store->contentLength);
    deleteHttpHeader(&res.header, (char*) 0, HTTPH_C_ENCODING); // 删掉Content-Encoding头
}

先解压,篡改完以后(理论上应该重新压缩,但代码里似乎依赖后续处理),再更新Content-Length。如果篡改后内容变长了,这个长度必须重新计算,否则浏览器会截断或等待超时。

六、那两个"不正经"的恶作剧是怎么实现的

Rickroll模式

代码里有一行很骚的操作:

if(scenarios & CL_RICKROLL)
    return setupRickRoll();

这个模式会把网页里所有的<a href="...">链接,全部替换成Rick Astley的YouTube视频链接。实现原理就是在网页HTML内容上做全局字符串替换,把href="原链接"改成href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"

Gravity模式(重力模式)

这个模式会往网页里注入一段JavaScript代码,让页面上的DOM元素受到"重力"影响往下掉。实现方式是在HTML的<head><body>里插入一个<script>标签,标签内是一段操作CSS transform或position的JS代码。

这两个模式本质上都是预设的篡改规则,只是比手动指定正则更方便,属于"一键搞事情"。


七、准备SSL证书 和 运行测试

.
├── commandline.c    # 命令行解析
├── commandline.h
├── http.c / http.h  # HTTP协议解析
├── ssl.c / ssl.h    # OpenSSL封装
├── regex.c / regex.h # 正则匹配
├── utils.c / utils.h # 工具函数
├── scenarios.c / scenarios.h # 恶作剧功能
├── proxy.c / proxy.h # 代理核心逻辑
└── data/            # 放证书的地方

If you need the complete source code, please add the WeChat number (c17865354792)

准备SSL证书

代码里默认会找这两个文件:

  • data/localhost.pem(CA证书)
  • data/privkey.pem(私钥)

如果目录里没有,自己生成一套自签名的:

mkdir -p data
cd data

# 生成私钥
openssl genrsa -out privkey.pem 2048

# 生成自签名证书(Common Name可以填localhost)
openssl req -new -x509 -key privkey.pem -out localhost.pem -days 365 \
  -subj "/C=US/ST=Test/L=Test/O=Test/CN=localhost"

cd ..

注意: 这套证书是"伪造"的。现代浏览器访问HTTPS网站时,会弹出大红警告(证书不受信任),这是正常的。如果你想让浏览器不报警,需要把localhost.pem安装到系统的"受信任的根证书颁发机构"里(后面会说)。


运行测试
1. 先看帮助
./prox

如果参数不够,程序会打印出你贴的那段帮助信息,告诉你怎么用。

2. 最简单的HTTP测试

先别急着搞HTTPS,先用HTTP试一把,确认代理能跑通。

启动代理(监听9999端口,目标是example.com):

./prox -p 9999 -r "Example Domain" -string "【你被我改了】" example.com

这个命令的意思是:

  • -p 9999:代理监听本地9999端口
  • -r "Example Domain":正则匹配网页里的"Example Domain"这串字
  • -string "【你被我改了】 ":把匹配到的内容替换成这串字
  • example.com:目标主机

测试方法A:用curl(最方便)

# 通过代理访问
http_proxy=http://127.0.0.1:9999 curl -v http://example.com

如果成功,你会在返回的HTML里看到 【你被我改了】 而不是原来的 “Example Domain”。

测试方法B:用浏览器

打开浏览器(建议先用命令行浏览器如lynxlinks测试,避免缓存干扰):

# 安装lynx
sudo apt-get install lynx

# 通过代理访问
lynx -proxy=http://127.0.0.1:9999 http://example.com

或者给Chrome/Firefox设置代理:

  • 地址:127.0.0.1
  • 端口:9999

然后访问 http://example.com,看看标题有没有被改掉。

3. 测试"恶作剧模式"

项目自带两个整活功能,一键启动:

Rickroll模式(把所有链接换成Rick Astley视频):

./prox -p 9999 -rickroll example.com

Gravity模式(给网页加物理重力效果):

./prox -p 9999 -gravity example.com

测试方法和上面一样,通过代理访问看效果。


HTTPS测试

HTTPS的问题是证书信任。因为代理会用自己生成的CA证书去冒充目标网站,浏览器会报警。

测试步骤:

1. 启动代理

./prox -p 9999 -r "<title>" -string "<title>【HTTPS也被改了】" -ca data/localhost.pem -pk data/privkey.pem example.com

2. 把证书导入浏览器/系统

不同系统方式不一样:

Linux(以Firefox为例):

  • 打开Firefox设置 → 隐私与安全 → 证书 → 查看证书
  • 导入 data/localhost.pem,勾选"信任此证书来标识网站"

Windows:

  • 双击 localhost.pem → 安装证书 → 选择"本地计算机" → 放进"受信任的根证书颁发机构"

macOS:

sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain data/localhost.pem

3. 浏览器设置代理后访问 https://example.com

如果证书导入成功,浏览器不会报红字,并且网页标题会被改成 【HTTPS也被改了】

如果没导入证书,浏览器会显示"您的连接不是私密连接",点高级→继续前往(不同浏览器叫法不一样)也能看到效果,只是每次都有警告。

八、知识要点总结:从这段代码能学到什么

如果你是想学习网络编程和安全攻防,这个项目虽然不大,但浓缩了很多实用知识点:

1. HTTP代理的本质是"双向搬运工"
代理不是简单的转发字节流,它必须完整解析HTTP请求头(特别是Host头),因为目标地址在请求头里,不在TCP层。对于HTTPS,还需要处理CONNECT方法建立隧道。

2. SSL中间人的核心是"证书信任链"
为什么浏览器会信任代理伪造的证书?因为代理自带了一个自签名CA,你需要事先把这个CA的根证书安装到浏览器/系统的信任列表里。一旦信任,代理就能为任意域名签发"合法"证书。这也解释了为什么企业内网能监控HTTPS流量——公司电脑预装了企业CA证书。

3. 内容篡改的三个前提

  • 必须能解密(对于HTTPS需要SSL中间人)
  • 必须能识别内容类型(通过Content-Type: text/html判断)
  • 必须能处理压缩(先解压再改,改完更新长度)

4. 多进程模型的利弊
利:代码简单,一个连接一个进程,天然隔离,崩溃安全。
弊:进程创建开销大,不适合高并发。现代高性能代理一般用事件驱动(epoll/kqueue)或线程池。但对于一个教学/演示性质的工具,多进程足够用了。

5. 正则表达式是文本篡改的瑞士军刀
程序不仅支持用户自定义POSIX正则,还内置了HTML标签和属性的正则生成器。理解regcompregexec在C中的用法,是学习底层文本处理的好例子。

6. 安全与攻击是一体两面
程序里删除Content-Security-Policy头的操作,是典型的"先拆盾再攻击"思路。Content-Security-Policy(CSP)是现代浏览器防止XSS和注入攻击的重要机制,删掉它,后续注入的脚本才能顺利执行。

Welcome to follow WeChat official account【程序猿编码

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐