本文主要分成三部分,第一部分阐述HTTP代理的应用场景和基础理论知识;第二部分介绍笔者项目中使用本地代理服务来代理WebView流量,实现在外网也能打开内网应用的案例;第三部分是介绍Chromium中关于代理模块的一些源码实现。
一、HTTP代理的知识
代理服务器的使用场景有很多,比如:
普通代理和隧道代理都是网络代理的一种形式,它们在处理客户端请求和数据传输方面有一些相同点和不同点。以下是对这两种代理的分别阐述:
普通代理:
普通代理,又称为正向代理,位于客户端和目标服务器之间。客户端将请求发送到代理服务器,代理服务器再将请求转发到目标服务器。目标服务器返回的响应同样经过代理服务器再返回给客户端。
来自《HTTP 权威指南》的定义是:
HTTP 客户端向代理发送请求报文,代理服务器需要正确地处理请求和连接(例如正确处理 Connection: keep-alive),同时向服务器发送请求,并将收到的响应转发给客户端
普通代理的主要特点:
隧道代理:
隧道代理是一种特殊类型的代理服务器,它在客户端和目标服务器之间建立一个透明的TCP隧道。客户端通过隧道与目标服务器建立直接的TCP连接,代理服务器不会修改或解析传输的数据。
来自《HTTP 权威指南》的定义是:
HTTP 客户端通过 CONNECT 方法请求隧道代理创建一条到达任意目的服务器和端口的 TCP 连接,并对客户端和服务器之间的后继数据进行盲转发。
隧道代理的主要特点:
相同点:
不同点:
当Chromium通过代理服务器发起请求,而该代理服务器需要认证时,会发生以下过程:
Proxy-Authenticate
头部,这个头部包含了代理服务器支持的认证方法(如Basic、Digest、NTLM或Negotiate)和其他认证信息。Proxy-Authorization
头部,并重新发送请求。对于基本认证(Basic),认证凭据是用户名和密码的Base64编码;对于摘要认证(Digest),认证凭据是用户名、密码、随机数等信息的摘要。以上就是Chromium处理代理服务器认证的基本过程。注意这个过程可能会因为代理服务器的配置和支持的认证方法而有所不同。
在Chromium中,向代理服务器发送流量与直接发送到目标服务器的过程有一些关键区别。以下是这两种情况下建立网络连接和发送请求的主要区别:
/index.html
)。http://example.com/index.html
)。此外,对于HTTP代理,Chromium会在HTTP请求头中添加Proxy-Connection
字段。Via
字段。笔者所在的项目中,一个网页代理的应用场景是:因为有一些页面是内网应用,在移动网络下无法访问,因此需要将内网应用的请求转发给内网的代理网关,其他的请求则可以把直接发送到外网。
我们的解决方案是建立一个App侧的本地代理服务,将WebView的流量都转发给本地代理服务处理,由本地代理服务决定是通过代理连接发送请求,还是直接发送请求。
于是我们将WebView的代理地址设置为本地地址127.0.0.1
,然后初始化一个本地HTTP SERVER来代理WebView的请求。对于本地代理服务,我们使用了基于libevent的C++实现,这样android、iOS和pc端都可以复用这个代理服务。下面是整体方案实现的时序图:
首先,需要设置WebView的代理地址和代理端口。因为我们是使用本地的代理服务,所以host设置为127.0.0.1
,端口则是随机选择一个空闲的端口号。
// 定义一个用于设置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)
}
}
主要代码逻辑摘要:
ProxyConfig.Builder
实例,并使用addProxyRule
方法添加代理服务器地址。urlsToProxy
数组并添加到代理配置的例外规则中。addDirect
方法,将直接连接添加到代理规则中。ProxyController
实例,并调用setProxyOverride
方法应用代理配置。设置代理完成后,执行回调函数。本地代理服务负责监听本地地址的流量,如果遇到需要转发到代理网关的url,则通过代理连接转发请求;否则就直接发送请求。这里的完整实现细节比较复杂,下面只展示了如何在APP侧使用libevent初始化一个HTTP SERVER。
下面的代码是一个名为InitHttpServer
的函数,其作用是初始化一个HTTP代理服务器。函数首先创建一个新的evhttp
实例,设置允许的HTTP方法,并尝试在指定IP地址上绑定一个端口。如果绑定失败,将尝试10次随机生成一个端口并绑定。成功绑定端口后,函数将显示监听的套接字信息,并返回0表示成功。如果在整个过程中出现错误,函数将返回相应的错误代码。
// 初始化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浏览器中,代理服务器的配置和使用是由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),那么该请求将直接发送到目标服务器。
当一个HTTP请求发起时,Chromium首先需要确定是否使用代理服务器。以下是Chromium将流量导向代理服务器的主要步骤:
ProxyConfigService
获取代理配置。这些配置可能来自用户设置或操作系统设置。ProxyConfigService
会返回一个ProxyConfig
实例,其中包含代理规则和例外列表。ProxyService
根据ProxyConfig
中的代理规则为HTTP请求选择合适的代理服务器。这个过程可能涉及解析PAC文件(通过ProxyResolverV8
)或者使用固定的代理规则(通过ProxyResolverFixed
)。ProxyService
会根据HTTP请求的URL和代理规则,为该请求选择一个合适的代理服务器。如果没有合适的代理服务器,或者配置了直接连接(DIRECT),那么该请求将直接发送到目标服务器。ClientSocketPoolManager
来管理网络连接。当需要使用代理服务器时,ClientSocketPoolManager
会为代理服务器创建一个新的ClientSocketHandle
。这个ClientSocketHandle
包含了代理服务器的IP地址和端口。Proxy-Connection
字段。对于SOCKS代理,Chromium会遵循SOCKS协议发送请求。通过以上步骤,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源码中的代理实现,由浅入深地展示了网络代理的理论和应用。希望可以帮助读者在实际场景中更好地利用代理服务器,实现相关的需求。