进阶-HTTP代理

提到http代理, 一般意义上理解的有很多种, 比较常见的理解是:

  • 对外发起http请求时, 使用ip代理
  • 类似nginx或CDN服务的反向代理
  • 建立一个代理服务, 让浏览器或其它设备可以通过该服务请求外界服务

惊喜的是, 以上3种不同场景的"代理", Z1h都提供了相关支持

对外请求使用IP代理

这一技术多用于爬虫, 例如待爬取的站点的某接口限制了同一IP的请求频率(x分钟内不能超过x次等), 就需要将自己的http请求通过别人提供的代理服务器来发送

使用方法

在调用几乎所有可以发起http的非原生方法时, 如果header包含_proxy_, 就等于指定了代理的地址

示例代码

// 普通http请求, 获取ip
print(`https://zwei.ren/ip`.get().s)

// 通过代理获取ip
print(`https://zwei.ren/ip`.get({
	_proxy_: 'http://xxx.xxx.xxx.xxx:xxxx',
	_timeout_: 1, // BTW: 可以通过这种方式设置超时时间
}).s)

// 同理, post请求往第二个参数设置代理
print(`https://zwei.ren/ip`.post('', {
	_proxy_: 'http://xxx.xxx.xxx.xxx:xxxx',
}).s)

反向代理

反向代理的用途很广, 例如以下场景:

  • 日常开发工作中, 前端需要和后端联调时, 后端还没有开发好接口, 或者需要修改接口但还没完成
  • 多人并行开发服务端时, 会出现抢占测试环境的情况, 需要针对一些路由或逻辑将端口转发到不同的内网地址
  • 需要将一个站点的各个文件完整保存下来

此时, 通过Z1h开启一个http服务, 并且将所有http请求都转发到某个地址, 即可完成反向代理

步骤1: 修改配置

首先要开启http服务, 并且修改api相关配置:

  • 修改配置文件
  • 设置port, 让Z1h以http服务的形式运行
  • 设置noFronttrue, 不对外输出静态站点资源
  • 设置serverless.api.router/, 意思是使用Z1h来处理全部接口
  • 设置serverless.api.dir, 例如设置到 datas/api, 接口将由datas/api/xxx.z1h处理

以上步骤完成后, 最简单的配置示例如下:

{
	port: 30030, // 运行端口号
	serverless: {
		api: {
			router: "/",
			dir: "datas/api",
		}
	},
	noFront: true,
}

现在, 可以用命令 ./z1h -conf conf.json运行Z1h服务

步骤2: 处理请求

接着创建接口处理文件:

  • 创建datas/api/404.z1h文件, 指定了无法找到请求接口文件时的默认处理
  • 如果你仅需要代理某路由, 可以指定文件名为路由, 如 api.z1h
  • 编辑404.z1h文件, 写入$http.proxy(this, 'http://xxx'), 其中xxx为你需要转发请求到的源站地址

Tips: 可以把xxx替换成https://www.baidu.com试试

步骤3: 测试

打开浏览器, 输入 http://127.0.0.1:30030, 即可看到反向代理后的结果(某些站点可能会让浏览器重定向, 可以用curl来请求接口测试)

中间人读取/修改双向数据

编辑404.z1h:

$http.proxy(this, 'http://xxx', (data, action) => {
	// action为request或response
	switch (action) {
		case 'request':
			// action为request时, data包含以下字段:
			// Url 		请求地址
			// Method 	请求方法
			// Header 	请求头
			// Body  	请求体([]byte)
			// Request 	原生请求对象
			// Client 	原生请求客户

			print(`请求到${data.url}, 方法${data.method}, 头部${data.header}, 内容${data.body.s}`)
			// 可以随意篡改请求内容
			data.Header.Set('X-HOOK-REQ', 'Z1h')
			// data.Method = 'POST'
			// data.Body = bytearray({name: 'zwr'})

			// 返回值不同类型时, 情况如下:
			// return null/true, 表示继续原来的请求
			// return false, 表示拦截请求, 即刻返回该内容
			// return *net_http.Response, 拦截请求, 返回指定的状态码等
			// 其它情况: 拦截请求, 立刻返回该内容
			return null
			// 可以试试 return {message: 'Forbidden'}
		case 'response':
			// action为request时, data为*net_http.Response对象

			print(`请求到${data.request.url.string()}的结果, 状态码${data.statusCode}`)

			// 可以修改响应信息
			data.Header.Set('X-HOOK-RSP', 'Z1h')
			if (data.statusCode == 301 || data.statusCode == 302)
				return `源站企图把你重定向到 ${data.header.get('Location')}, 已经被阻止了`


			// 返回值不同类型时, 情况与上文request相同, 区别是此时请求已经由源站处理过了
			return null
			// 可以试试 return {message: 'Done'}
	}
})

// 只拦截请求
$http.proxy(this, 'http://xxx', {
	request: data => {
		// TODO
	},
})

// 只拦截响应
$http.proxy(this, 'http://xxx', {
	response: data => {
		// TODO
	},
})

自制简单的CDN

照样是在前文的基础上编辑:

var host = 'http://xxx'
var dir = 'cdn'.mkdir() // 使用一个文件来保存
$http.proxy(this, host, {
	request: data => {
		print(`Req to:`, data.url)
		var path = data.url.url().path
		if (!path || path == "/")
			path = "index.html"
		var cacheFile = $file.join(dir, path) // 会忽略掉请求参数,建议读者动手尝试一下根据参数来保存和读取
		if ($file.exists(cacheFile) == 1) {
			// 如果文件已经存在, 响应文件并拦截后续请求
			print(`文件${path}使用本地文件`)
			data.request.serveFile(cacheFile)
			return false
		}
	},
	res: res => {
		if (res.statusCode < 200 || res.statusCode > 399) {
			print(`请求${res.Request.URL.String()}的状态码为${res.statusCode}, 不保存文件`)
			return true
		}
		print(`正在补充${res.Request.URL.String()}的文件`)
		var body = res.origin()
		if (body.len) {
			// 进行一些内容上的处理, 确保浏览器能正常打开
			body = body
				.replace('https://', 'http://')
				.replace('https', 'http')
				.replace(host + '/{0,}', "/")
			// 保存到本地文件
			var path = res.request.url.path
			if (!path || path == "/")
				path = "index.html"
			var cacheFile = $file.join(dir, path) // 会忽略掉请求参数,建议读者动手尝试一下根据参数来保存
			path_filepath.Dir(cacheFile).mkdir()
			cacheFile.write(body)
			// 下面要去掉encoding, 如果gzip了的话, Content-Length会有变化
			res.header.Set('Content-Length', body.len.s)
			res.body = reader(body)
		} else {
			res.Body = reader('')
		}
		res.Header.Del('Content-Encoding')
	},
})

建立一个HTTP代理并进行MITM

这是一个很强大的功能, 如果你有Charles、Flidder、Wireshark等抓包工具的使用经验, 甚至有尝试过嵌入脚本修改请求、返回数据的话, 那么你一定非常清楚这个工具的便利

通过Z1h建立HTTP代理服务, 你可以做到(且不限于)以下事情:

  • 抓包, 和其它抓包工具一致
  • 数据处理, 例如将所有数据都保存、分析
  • 监听, 可以监控敏感站点、过滤广告、过滤(或收集)不良信息等
  • 篡改请求数据, 或拦截请求mock数据返回
  • 篡改响应数据, 或拦截响应
  • 支持http和https, https需要自己创建证书

暂不支持websocket的代理, 后续会跟进

文档待续