本文主要分成三部分,第一部分阐述HTTP代理的应用场景和基础理论知识;第二部分介绍笔者项目中使用本地代理服务来代理WebView流量,实现在外网也能打开内网应用的案例;第三部分是介绍Chromium中关于代理模块的一些源码实现。
一、HTTP代理的知识
1.1 代理服务器的应用场景
代理服务器的使用场景有很多,比如:
- 在公司或学校网络中,可能需要通过代理服务器才能访问Internet。
- 为了保护隐私,用户可能会使用代理服务器来隐藏自己的IP地址。
- 为了绕过地理限制,用户可能会使用位于特定国家/地区的代理服务器来访问某些网站或服务。
- 开发者可能会使用代理服务器来调试HTTP请求和响应。
1.2 普通代理和隧道代理
普通代理和隧道代理都是网络代理的一种形式,它们在处理客户端请求和数据传输方面有一些相同点和不同点。以下是对这两种代理的分别阐述:
普通代理:
普通代理,又称为正向代理,位于客户端和目标服务器之间。客户端将请求发送到代理服务器,代理服务器再将请求转发到目标服务器。目标服务器返回的响应同样经过代理服务器再返回给客户端。
来自《HTTP 权威指南》的定义是:
HTTP 客户端向代理发送请求报文,代理服务器需要正确地处理请求和连接(例如正确处理 Connection: keep-alive),同时向服务器发送请求,并将收到的响应转发给客户端。
普通代理的主要特点:
- 代理服务器可以修改客户端的请求和目标服务器的响应,例如添加、删除或修改HTTP头部。
- 代理服务器可以缓存目标服务器的响应,以提高访问速度和降低网络带宽消耗。
- 代理服务器可以对HTTP请求进行过滤和审计,实现访问控制和安全策略。
隧道代理:
隧道代理是一种特殊类型的代理服务器,它在客户端和目标服务器之间建立一个透明的TCP隧道。客户端通过隧道与目标服务器建立直接的TCP连接,代理服务器不会修改或解析传输的数据。
来自《HTTP 权威指南》的定义是:
HTTP 客户端通过 CONNECT 方法请求隧道代理创建一条到达任意目的服务器和端口的 TCP 连接,并对客户端和服务器之间的后继数据进行盲转发。
隧道代理的主要特点:
- 代理服务器不会修改或解析通过隧道传输的数据,只负责传输数据包。
- 隧道代理通常用于建立安全连接(如SSL/TLS),在此情况下,代理服务器无法查看或修改加密的数据。
- 隧道代理可以穿越防火墙和NAT设备,访问内网或受限的网络资源。
相同点:
- 两者都位于客户端和目标服务器之间,起到中介的作用。
- 两者都可以用于访问控制、隐私保护和绕过网络限制。
不同点:
- 普通代理可以修改客户端请求和目标服务器响应,而隧道代理只负责传输数据包,不会修改或解析数据。
- 普通代理可以缓存和过滤HTTP请求,而隧道代理通常不具备这些功能。
- 隧道代理主要用于建立安全连接和穿越网络障碍,而普通代理更关注于请求处理和响应缓存。
1.3 代理服务器认证过程
当Chromium通过代理服务器发起请求,而该代理服务器需要认证时,会发生以下过程:
- 收到407响应:当Chromium发送请求到需要认证的代理服务器时,代理服务器会返回一个407 Proxy Authentication Required的响应。
- 读取代理认证信息 :Chromium会从407响应中读取
Proxy-Authenticate
头部,这个头部包含了代理服务器支持的认证方法(如Basic、Digest、NTLM或Negotiate)和其他认证信息。 - 选择认证方法:Chromium会选择一个支持的认证方法。如果Chromium不支持代理服务器要求的任何认证方法,它将无法通过代理服务器发送请求。
- 获取认证凭据:Chromium会尝试从代理设置中获取用户名和密码作为认证凭据。如果代理设置中没有提供认证凭据,Chromium可能会显示一个对话框,提示用户输入用户名和密码。
- 发送认证凭据 :Chromium会将认证凭据添加到请求的
Proxy-Authorization
头部,并重新发送请求。对于基本认证(Basic),认证凭据是用户名和密码的Base64编码;对于摘要认证(Digest),认证凭据是用户名、密码、随机数等信息的摘要。 - 处理认证结果:如果认证成功,代理服务器会返回200 OK的响应,并将请求转发到目标服务器;如果认证失败,代理服务器会再次返回407响应,Chromium可以选择重试认证或者放弃请求。
以上就是Chromium处理代理服务器认证的基本过程。注意这个过程可能会因为代理服务器的配置和支持的认证方法而有所不同。
1.4 代理连接与直接连接的区别
在Chromium中,向代理服务器发送流量与直接发送到目标服务器的过程有一些关键区别。以下是这两种情况下建立网络连接和发送请求的主要区别:
-
建立连接:
- 直接连接:Chromium会根据目标服务器的URL解析出的IP地址和端口建立一个TCP连接。
- 代理连接:Chromium会根据代理服务器的IP地址和端口建立一个TCP连接。
-
发送请求:
- 直接连接:Chromium将HTTP请求发送到目标服务器。请求行中的URL使用相对路径(如
/index.html
)。 - 代理连接:Chromium将HTTP请求发送到代理服务器。请求行中的URL使用完整路径(如
http://example.com/index.html
)。此外,对于HTTP代理,Chromium会在HTTP请求头中添加Proxy-Connection
字段。
- 直接连接:Chromium将HTTP请求发送到目标服务器。请求行中的URL使用相对路径(如
-
处理响应:
- 直接连接:目标服务器直接将HTTP响应发送回Chromium。
- 代理连接:代理服务器将请求转发到目标服务器,然后将目标服务器的响应返回给Chromium。在这个过程中,代理服务器可能会修改响应头部,例如添加
Via
字段。
-
安全连接(HTTPS):
- 直接连接:Chromium会与目标服务器建立SSL/TLS连接,然后在安全连接上发送HTTP请求。
- 代理连接:Chromium会使用CONNECT方法与代理服务器建立一个TCP隧道,然后在隧道上建立SSL/TLS连接。在安全连接上发送HTTP请求时,代理服务器无法查看或修改请求内容。
-
认证:
- 直接连接:如果目标服务器需要认证,Chromium会处理服务器返回的401 Unauthorized响应。
- 代理连接:如果代理服务器需要认证,Chromium会处理代理服务器返回的407 Proxy Authentication Required响应。
二、如何在Android中建立WebView的本地代理
2.1 案例背景
笔者所在的项目中,一个网页代理的应用场景是:因为有一些页面是内网应用,在移动网络下无法访问,因此需要将内网应用的请求转发给内网的代理网关,其他的请求则可以把直接发送到外网。
2.2 解决方案一览
我们的解决方案是建立一个App侧的本地代理服务,将WebView的流量都转发给本地代理服务处理,由本地代理服务决定是通过代理连接发送请求,还是直接发送请求。
于是我们将WebView的代理地址设置为本地地址127.0.0.1
,然后初始化一个本地HTTP SERVER来代理WebView的请求。对于本地代理服务,我们使用了基于libevent的C++实现,这样android、iOS和pc端都可以复用这个代理服务。下面是整体方案实现的时序图:
2.3 设置WebView的代理地址和代理端口
首先,需要设置WebView的代理地址和代理端口。因为我们是使用本地的代理服务,所以host设置为127.0.0.1
,端口则是随机选择一个空闲的端口号。
kotlin
// 定义一个用于设置WebView代理的函数
fun setProxyOverrideSYS(urlsToProxy: Array<String>?, proxy: String, calback: Runnable) {
// 检查WebView是否支持代理覆盖功能
try {
if (ReflecterHelper.invokeStaticMethod("androidx.webkit.WebViewFeature", "isFeatureSupported", arrayOf(WebViewFeature.PROXY_OVERRIDE)) as? Boolean != true) {
Log.d(TAG, "setProxyOverrideSYS", "no support")
return
}
} catch (e: Throwable) {
Log.d(TAG, "setProxyOverrideSYS", e)
return
}
// 创建ProxyConfig.Builder实例并添加代理规则
var builder: Any? = null
try {
builder = ReflecterHelper.newInstance("androidx.webkit.ProxyConfig\$Builder")
ReflecterHelper.invokeMethod(builder, "addProxyRule", arrayOf(proxy))
} catch (e: Throwable) {
Log.d(TAG, "setProxyOverrideSYS", e)
return
}
// 检查WebView是否支持反向代理覆盖功能并添加例外规则
try {
if (ReflecterHelper.invokeStaticMethod("androidx.webkit.WebViewFeature", "isFeatureSupported", arrayOf(WebViewFeature.PROXY_OVERRIDE_REVERSE_BYPASS)) as? Boolean == true) {
urlsToProxy?.forEach {
if (!TextUtils.isEmpty(it)) {
ReflecterHelper.invokeMethod(builder, "addBypassRule", arrayOf(it))
ReflecterHelper.invokeMethod(builder, "setReverseBypassEnabled", arrayOf(true))
}
}
} else {
Log.d(TAG, "setProxyOverrideSYS", "bypass no support")
}
} catch (e: Throwable) {
Log.d(TAG, "setProxyOverrideSYS", e)
}
// 将直接连接添加到代理规则中
try {
ReflecterHelper.invokeMethod(builder, "addDirect")
} catch (e: Throwable) {
Log.d(TAG, "setProxyOverrideSYS", e)
return
}
// 应用代理配置并设置回调函数
try {
val controller = ReflecterHelper.invokeStaticMethod("androidx.webkit.ProxyController", "getInstance")
val config = ReflecterHelper.invokeMethod(builder, "build")
ReflecterHelper.invokeMethod(controller, "setProxyOverride", arrayOf(config.javaClass, Executor::class.java, Runnable::class.java), arrayOf(config, Executor { command ->
Log.d(TAG, "setProxyOverrideSYS execute")
try {
command.run()
} catch (e: Throwable) {
Log.d(TAG, "setProxyOverrideSYS execute", e)
}
}, calback))
} catch (e: Throwable) {
Log.d(TAG, "setProxyOverrideSYS", e)
}
}
主要代码逻辑摘要:
- 检查WebView是否支持代理覆盖功能。
- 创建一个
ProxyConfig.Builder
实例,并使用addProxyRule
方法添加代理服务器地址。 - 检查WebView是否支持反向代理覆盖功能。如果支持,遍历
urlsToProxy
数组并添加到代理配置的例外规则中。 - 调用
addDirect
方法,将直接连接添加到代理规则中。 - 获取
ProxyController
实例,并调用setProxyOverride
方法应用代理配置。设置代理完成后,执行回调函数。
2.4 在APP侧建立本地代理服务
本地代理服务负责监听本地地址的流量,如果遇到需要转发到代理网关的url,则通过代理连接转发请求;否则就直接发送请求。这里的完整实现细节比较复杂,下面只展示了如何在APP侧使用libevent初始化一个HTTP SERVER。
下面的代码是一个名为InitHttpServer
的函数,其作用是初始化一个HTTP代理服务器。函数首先创建一个新的evhttp
实例,设置允许的HTTP方法,并尝试在指定IP地址上绑定一个端口。如果绑定失败,将尝试10次随机生成一个端口并绑定。成功绑定端口后,函数将显示监听的套接字信息,并返回0表示成功。如果在整个过程中出现错误,函数将返回相应的错误代码。
cpp
// 初始化HTTP代理服务器
int ProxyContext::InitHttpServer(struct evhttp **http_server, int &port)
{
// 创建一个新的evhttp实例
*http_server = evhttp_new(base);
if (!proxy_http) {
log_error(this, "couldn't create proxy_http. Exiting.");
return -2;
}
// 设置允许的HTTP方法
evhttp_set_allowed_methods(*http_server,
EVHTTP_REQ_PUT|
EVHTTP_REQ_DELETE|
EVHTTP_REQ_OPTIONS|
EVHTTP_REQ_TRACE|
EVHTTP_REQ_PATCH|
EVHTTP_REQ_CONNECT|
EVHTTP_REQ_GET|
EVHTTP_REQ_POST|
EVHTTP_REQ_HEAD);
struct evhttp_bound_socket *handle = NULL;
// 尝试10次绑定端口
for (int i = 0; i < 10; i++) {
// 如果端口为0,随机生成一个10000-30000之间的端口
if (port == 0) {
port = (rand()%20000) + 10000;
}
// 尝试绑定端口
handle = evhttp_bind_socket_with_handle(*http_server, PROXY_IP_ADDRESS, port);
if (!handle) {
log_error(this, "couldn't bind to port:%d. Exiting.", port);
port = 0;
continue;
}
break;
}
// 如果无法绑定端口,返回错误
if (port == 0) {
log_error(this, "couldn't get a right port");
return -4;
}
// 如果无法显示监听的套接字信息,返回错误
if (display_listen_sock(this, handle)) {
log_error(this, "display_listen_sock error");
return -5;
}
// 成功返回
return 0;
}
三、Chromium如何实现代理连接
3.1 获取与解析代理配置
在Chromium浏览器中,代理服务器的配置和使用是由ProxyService类来管理的,它的源码位于net/proxy目录下。ProxyService类的主要职责是根据用户的配置或者操作系统的配置,为每一个HTTP请求选择合适的代理服务器。
当一个HTTP请求发起时,ProxyService
会首先查询代理设置,这些设置可能来自用户在浏览器中的手动设置,也可能来自操作系统的代理设置。在Unix-like系统中,这些设置通常来自环境变量http_proxy
、https_proxy
等;在Windows系统中,这些设置来自Internet选项中的局域网设置。
ProxyService
会解析这些代理设置,生成一个或多个ProxyServer
实例。每个ProxyServer
实例代表一个代理服务器,包含代理服务器的协议(如HTTP、SOCKS4、SOCKS5等)、主机名和端口。
ProxyService
会根据HTTP请求的URL和代理规则,为该请求选择一个合适的ProxyServer
。代理规则可以包括一些例外情况,比如某些域名不使用代理。如果没有合适的代理服务器,或者配置了直接连接(DIRECT),那么该请求将直接发送到目标服务器。
3.2 Chromium将流量导向代理服务器的过程
当一个HTTP请求发起时,Chromium首先需要确定是否使用代理服务器。以下是Chromium将流量导向代理服务器的主要步骤:
- 获取代理配置 :Chromium通过
ProxyConfigService
获取代理配置。这些配置可能来自用户设置或操作系统设置。ProxyConfigService
会返回一个ProxyConfig
实例,其中包含代理规则和例外列表。 - 解析代理规则 :
ProxyService
根据ProxyConfig
中的代理规则为HTTP请求选择合适的代理服务器。这个过程可能涉及解析PAC文件(通过ProxyResolverV8
)或者使用固定的代理规则(通过ProxyResolverFixed
)。 - 选择代理服务器 :
ProxyService
会根据HTTP请求的URL和代理规则,为该请求选择一个合适的代理服务器。如果没有合适的代理服务器,或者配置了直接连接(DIRECT),那么该请求将直接发送到目标服务器。 - 建立连接 :Chromium使用
ClientSocketPoolManager
来管理网络连接。当需要使用代理服务器时,ClientSocketPoolManager
会为代理服务器创建一个新的ClientSocketHandle
。这个ClientSocketHandle
包含了代理服务器的IP地址和端口。 - 发送请求 :Chromium将HTTP请求发送到代理服务器。如果代理服务器需要认证,Chromium会处理认证过程。对于HTTP代理,Chromium会在HTTP请求头中添加
Proxy-Connection
字段。对于SOCKS代理,Chromium会遵循SOCKS协议发送请求。 - 接收响应:代理服务器将请求转发到目标服务器,并将目标服务器的响应返回给Chromium。Chromium会处理响应,解析页面内容并呈现给用户。
通过以上步骤,Chromium可以将流量导向代理服务器,实现在不同网络环境下的访问控制、隐私保护等功能。
3.3 Chromium中的代理服务器源码文件
Chromium中的net/proxy
目录下包含了与代理服务器相关的源码文件。以下是一些主要文件及其对应的功能:
proxy_config.cc
/proxy_config.h
:ProxyConfig
类表示代理配置,包括代理规则和例外列表。这些配置可以来自用户设置或操作系统设置。proxy_config_service.cc
/proxy_config_service.h
:ProxyConfigService
类是一个抽象类,用于获取当前的ProxyConfig
。具体的实现可能会依赖于操作系统或用户设置。proxy_info.cc
/proxy_info.h
:ProxyInfo
类包含了为特定URL选择的代理服务器信息。在发起HTTP请求时,ProxyService
会使用ProxyInfo
来确定使用哪个代理服务器。proxy_list.cc
/proxy_list.h
:ProxyList
类表示一组备选的代理服务器。在某些情况下,可能有多个代理服务器可供选择,ProxyList
提供了从中选择一个可用代理的功能。proxy_service.cc
/proxy_service.h
:ProxyService
类负责根据代理配置为HTTP请求选择合适的代理服务器。它使用ProxyConfigService
来获取代理配置,并将其应用到HTTP请求。proxy_server.cc
/proxy_server.h
:ProxyServer
类表示一个具体的代理服务器,包括代理协议(如HTTP、SOCKS4、SOCKS5等)、主机名和端口。proxy_resolver.cc
/proxy_resolver.h
:ProxyResolver
类是一个抽象类,用于解析代理规则。具体的实现可能包括PAC文件解析(proxy_resolver_v8.cc
/proxy_resolver_v8.h
)或者固定的代理规则(proxy_resolver_fixed.cc
/proxy_resolver_fixed.h
)。
这些文件共同构成了Chromium处理代理服务器的逻辑。要深入了解这些文件的具体实现,建议阅读Chromium的源码以获取更详细的信息。
四、总结
本文围绕网络代理的相关内容,先阐述理论基础,然后给出笔者项目中一个WebView代理的具体案例,最后深入到Chromium源码中的代理实现,由浅入深地展示了网络代理的理论和应用。希望可以帮助读者在实际场景中更好地利用代理服务器,实现相关的需求。