项目托管地址:https://github.com/sometiny/socks5
在前面文章,我们实现了Socks5服务器:C#实现Socks5服务器
现在我们让服务器提供Socks5服务的同时提供PAC服务。
0、PAC文件
包含类似于如下javascript代码,核心是FindProxyForURL
方法,执行完后,告诉浏览器该怎么处理用户发起的请求。
如果是DIRECT
,直连服务器;如果是SOCKS5
,则向指定的SOCKS5
服务器发起代理请求(协议的交换在前面文章有实现)。
var domains = {
"google.com" : 1,
"packagist.org" : 1,
"gmail.com" : 1,
"github.com" : 1
};
var proxy = 'SOCKS5 127.0.0.1:4088;';
var direct = 'DIRECT;';
var hasOwnProperty = Object.hasOwnProperty;
function FindProxyForURL(url, host) {
var suffix;
var pos = host.lastIndexOf('.');
pos = host.lastIndexOf('.', pos - 1);
while(1) {
if (pos <= 0) {
if (hasOwnProperty.call(domains, host)) {
return proxy;
} else {
return direct;
}
}
suffix = host.substring(pos + 1);
if (hasOwnProperty.call(domains, suffix)) {
return proxy;
}
pos = host.lastIndexOf('.', pos - 1);
}
}
1、可控制数据消费的流
我们目的就是在同一个端口,即提供Socks5服务,又提供PAC服务。
因为默认的NetworkStream是单向、不可逆的,所以在流BufferedNetworkStream中,我们通过Consume
属性来控制流是否消费缓冲区的数据。
主要在下面两个方法控制缓冲区的消费。
public override int ReadByte()
{
if (_length > 0)
{
if (!_consume) return _buffer[_offset];
_length--;
return _buffer[_offset++];
}
return base.ReadByte();
}
private int CopyFromBuffer(byte[] buffer, int offset, int size)
{
//如果要求读的数据超过了缓冲区内数据的大小,则只返回缓冲区内的可用数据
if (size > _length) size = _length;
//从缓冲区拷贝数据到应用
Array.Copy(_buffer, _offset, buffer, offset, size);
if (!_consume) return size;
//数据拷贝完成,移动偏移,修改缓冲区的可能数据长度
_offset += size;
_length -= size;
return size;
}
2、PAC服务实现
实现上面的流后,我们就可以从缓冲区中读取数据,通过判断流的起始数据来决定提供Socks5服务还是PAC服务。
这里判断的核心就是读取第一个字节,如果是0x05
,代表是Socks5服务,否则我们全部当成PAC服务来处理。
我们把上篇文章使用的NetworkStream
替换为BufferedNetworkStream
。
通过Consume
属性告诉流什么时候开始消费数据。
/// <summary>
/// 实现NewClient方法,处理Socks5请求
/// </summary>
/// <param name="client"></param>
protected override void NewClient(Socket client)
{
if (string.IsNullOrEmpty(_serveAt))
{
_serveAt = $"127.0.0.1:{LocalEndPoint.Port}";
}
//实例化的是BufferedNetworkStream,可控制数据的消费和缓冲区
BufferedNetworkStream stream = new BufferedNetworkStream(client, true);
try
{
//先设置基础流不消费读取到的缓冲区
stream.Consume = false;
int firstByte = stream.ReadByte();
//开始消费缓冲区
stream.Consume = true;
//第一个字节不为0x05,代表不是Socks5协议,我们全部作为PAC服务器处理
if (firstByte != 0x05)
{
Pac.Process(stream, _hostListFile, _serveAt);
return;
}
ProtocolExchanger exchanger = new ProtocolExchanger();
try
{
//实例化NetWorkStream,让实例化NetWorkStream拥有基础Socket的处理权限
exchanger.Start(stream);
}
catch
{
//异常,销毁
exchanger.Dispose();
}
}
catch
{
stream.Close();
}
}
PAC本质就是接收一个HTTP请求,发送一个HTTP响应到客户端。
在https://github.com/sometiny/socks5/blob/main/src/Pac.cs这里实现HTTP的简单读取和响应,就不把代码拷贝过来占用文章篇幅了。
3、测试
本地的pac.lst
文件把baidu.com
加上(https://github.com/sometiny/socks5/blob/main/bin/Release/pac.lst)。
启动服务,浏览器访问:http://127.0.0.1:4088/pac
可以看到浏览器显示了正确的PAC文件内容。
然后设置浏览器代理,脚本地址填写我们的PAC文件地址。
保存,浏览器访问:https://www.baidu.com
可以看到,baidu页面正常打开,开发工具中显示的Remote Address也是我们的Socks服务器地址,代理成功。
4、总结
1、PAC和Socks5完全可以分开,只要PAC中指定了正确的Socks服务器即可,我们为了少占用一个端口,放在一起方便处理。
2、同时提供两种服务的关键就是保留从基础流读取的数据,继续提供给下游应用使用,我们这里通过缓冲区和流是否消费缓冲区数据实现。
3、能够实现同时提供两种服务必须要求两种服务的协议有明显区别。