之前提到Android
的URLConnection
因为不支持ALPN
,从而不支持HTTP/2
。而OkHttp 2.5
以上,是支持ALPN
和HTTP/2
,但对平台有要求,限于Android 5.0
以上。那为什么会有平台的差异呢,度娘上有人说,是因为openssl
,需要openssl 1.0.2
以上才支持TLS
,而我手上的Android 4.4
用的是openssl 1.0.1e
,是不是这个原因呢,我们先从OkHttp
入手。
一、相关接口是否存在?
在OkHttp
的源码okhttp3.internal.platform.Platform.java
中
注释中有这么一段:
Access to platform-specific features. … ALPN (Application Layer Protocol Negotiation)
Supported on Android 5.0+. The APIs were present in Android 4.4, but that implementation was unstable.
Supported on OpenJDK 7 and 8 (via the JettyALPN-boot library).
Supported on OpenJDK 9 via SSLParameters and SSLSocket features. …
意思看起来是说,这部分的API
在4.4
里面是存在的,只是执行不稳定???不稳定,那还是有的嘛。继续看Platform.java
:
/** Attempt to match the host runtime to a capable Platform implementation. */
private static Platform findPlatform() {
Platform android = AndroidPlatform.buildIfSupported();
if (android != null) {
return android;
}
Platform jdk9 = Jdk9Platform.buildIfSupported();
if (jdk9 != null) {
return jdk9;
}
Platform jdkWithJettyBoot = JdkWithJettyBootPlatform.buildIfSupported();
if (jdkWithJettyBoot != null) {
return jdkWithJettyBoot;
}
// Probably an Oracle JDK like OpenJDK.
return new Platform();
}
可以看到,Android
平台的适配是通过AndroidPlatform.java
实现,在buildIfSupported()
方法中:
public static Platform buildIfSupported() {
// Attempt to find Android 2.3+ APIs.
try {
Class<?> sslParametersClass;
try {
sslParametersClass = Class.forName("com.android.org.conscrypt.SSLParametersImpl");
} catch (ClassNotFoundException e) {
// Older platform before being unbundled.
sslParametersClass = Class.forName("org.apache.harmony.xnet.provider.jsse.SSLParametersImpl");
}
OptionalMethod<Socket> setUseSessionTickets = new OptionalMethod<>(null, "setUseSessionTickets", boolean.class);
OptionalMethod<Socket> setHostname = new OptionalMethod<>(null, "setHostname", String.class);
OptionalMethod<Socket> getAlpnSelectedProtocol = null;
OptionalMethod<Socket> setAlpnProtocols = null;
// Attempt to find Android 5.0+ APIs.
try {
Class.forName("android.net.Network"); // Arbitrary class added in Android 5.0.
getAlpnSelectedProtocol = new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol");
setAlpnProtocols = new OptionalMethod<>(null, "setAlpnProtocols", byte[].class);
} catch (ClassNotFoundException ignored) {
}
return new AndroidPlatform(sslParametersClass, setUseSessionTickets, setHostname, getAlpnSelectedProtocol, setAlpnProtocols);
} catch (ClassNotFoundException ignored) {
// This isn't an Android runtime.
}
return null;
}
看到没,OkHttp
通过查找一个在Android 5.0+
才存在的类android.net.Network.java
,来判断平台版本,而从指定类中反射出两个方法来,其中关键的是getAlpnSelectedProtocol()
, 这里只是缓存了一个可选的方法名、返回值、参数等等,在后面会通过反射调用到,那这个指定的类是什么呢,先别管,我们先把Class.forName("android.net.Network");
这行注释掉,让他强制跑在4.4
上,看看是否前面说的,API
是存在的,但不稳定的,找getAlpnSelectedProtocol()
具体调用的地方:
@Override public String getSelectedProtocol(SSLSocket socket) {
if (getAlpnSelectedProtocol == null) return null;
if (!getAlpnSelectedProtocol.isSupported(socket)) return null;
byte[] alpnResult = (byte[]) getAlpnSelectedProtocol.invokeWithoutCheckedException(socket);
return alpnResult != null ? new String(alpnResult, Util.UTF_8) : null;
}
isSupported()
通过反射判断target
是否存在相应的方法:
/**
* Returns true if the method exists on the supplied {@code target}.
*/
public boolean isSupported(T target) {
return getMethod(target.getClass()) != null;
}
debug
模式运行到这里我们发现:
上面是4.4
的运行结果,可以执行通过,只是得到的结果alpnResult
是空的,而在5.0
的机器上,这里是有值的,解析成字符串,是h2
:
看来正如官方文档所说,getAlpnSelectedProtocol()
这个接口在4.4
上存在的,只是因为没有得到正确的结果,返回为空,所以unstable
?
二、为什么getAlpnSelectedProtocol返回为空?
接下来我们看看是哪个类持有这个接口,debug
可以看到:
原来是一个叫com.android.org.conscrypt.OpenSSLSocketImplWrapper
的类,那么这个类的源码在那里呢,很遗憾,Android SDK
开发包提供下载的sources
里面是找不到的。。。它在这里:
所以我们要找conscrypt.jar
的源码, OpenSSLSocketImplWrapper
其实是OpenSSLSocketImpl
的子类:https://android.googlesource.com/platform/external/conscrypt/+/android-5.0.0_r1/src/main/java/org/conscrypt/OpenSSLSocketImpl.java
还有4.4
的,我可是翻了很久才找到的:
https://android.googlesource.com/platform/external/conscrypt/+/e75878c72b717696d7e4f6cc1052f1cdaca3bda8/src/main/java/org/conscrypt/OpenSSLSocketImpl.java
两者的实现差不多,都包含:
/**
* Returns the protocol agreed upon by client and server, or {@code null} if
* no protocol was agreed upon.
*/
public byte[] getAlpnSelectedProtocol() {
return NativeCrypto.SSL_get0_alpn_selected(sslNativePointer);
}
从注释可以看到,是需要客户端和服务端都同意ALPN
,才会有返回值,具体实现,是个Native
方法:
/**
* Returns the selected ALPN protocol. If the server did not select a
* protocol, {@code null} will be returned.
*/
public static native byte[] SSL_get0_alpn_selected(long sslPointer);
那这个native
方法指向哪里呢,没错,它指向了openssl
,
openssl-1.0.2j
下载:https://www.openssl.org/source/openssl-1.0.2j.tar.gz
openssl-1.0.1e
下载:https://www.openssl.org/source/old/1.0.1/openssl-1.0.1e.tar.gz
在openssl-1.0.2j\include\openssl\ssl.h
中可以找到:
void SSL_get0_alpn_selected(const SSL *ssl, const unsigned char **data,
unsigned *len);
而在openssl-1.0.1e\include\openssl\ssl.h
中:
我勒个去,Unable to find…
原来这就是unstable
的原因啊!!!
三、替换openssl
既然Android 4.4
的openssl-1.0.1e
缺少SSL_get0_alpn_selected
,那我们换成openssl-1.0.2j
试试。
编译过程在这里:Android Openssl交叉编译
另外,Openssl
官方wiki
有个Android
环境编译教程:
https://wiki.openssl.org/index.php/Android#Acquire_the_Required_Files
可惜编译出来的没法使用:
CANNOT LINK EXECUTABLE: could not load library "libandroid_runtime.so" needed by "app_process"; caused by could not load library "libcrypto.so" needed by "libandroid_runtime.so"; caused by "libcrypto.so" has bad ELF magic
或者caused by "libssl.so" has bad ELF magic
替换完成后debug
到getAlpnSelectedProtocol()
:
alpnResult
还是null
。。。最终协商结果还是HTTP/1.1
:
D/—wyf—: Protocol: http/1.1
四、这到底是为什么
从前面getAlpnSelectedProtocol()
方法的注释说明:
/**
* Returns the protocol agreed upon by client and server, or {@code null} if
* no protocol was agreed upon.
*/
public byte[] getAlpnSelectedProtocol() {
return NativeCrypto.SSL_get0_alpn_selected(sslNativePointer);
}
还有SSL_get0_alpn_selected
的注释说明:
/*
* SSL_get0_alpn_selected gets the selected ALPN protocol (if any) from
* |ssl|. On return it sets |*data| to point to |*len| bytes of protocol name
* (not including the leading length-prefix byte). If the server didn't
* respond with a negotiated protocol then |*len| will be zero.
*/
void SSL_get0_alpn_selected(const SSL *ssl, const unsigned char **data, unsigned int *len)
{
*data = NULL;
if (ssl->s3)
*data = ssl->s3->alpn_selected;
if (*data == NULL)
*len = 0;
else
*len = ssl->s3->alpn_selected_len;
}
可以看到,是需要客户端和服务端都同意ALPN
协商,而SSL_get0_alpn_selected
只是从协商结果中判断是否能使用HTTP/2
,难道不是openssl
的问题?那问题出在哪呢
五、TLS链接建立和ALPN协商过程
看来有必要从头了解一下HTTP/2
请求过程,我们在浏览器上访问一下HTTP/2
网站,抓包看看:
首先,Client
向Server
发送一个ClientHello
消息,说明它支持的密码算法列表、压缩方法及最高协议版本,以及稍后将被使用的随机数:
在TLSv1.2
中,会有个扩展字段Extension
,通过ALPN
扩展列出了自己支持的各种应用层协议,比如h2
、http/1.1
:
然后服务端会在Server Hello
中选取所支持的加密算法和密钥大小,以及支持的协议,如果服务端支持HTTP/2
,指定ALPN
的协商结果为h2
就可以了;如果服务端不支持HTTP/2
,就会从客户端的 ALPN
支持列表中选一个自己可以支持的。比如下面这图,服务端选择了降级成HTTP/1.1
:
这就是TLS
链接建立ALPN
协商的过程。
其中ALPN
被包含了在TLS v1.22
的Extension
字段中,查看TLS wiki,在1.2
以前的版本是没有Extension
字段的,看来问题的关键在于TLS
的版本。
六、TLS支持
我们来看看Android
所支持的TLS
版本:https://developer.android.com/reference/javax/net/ssl/SSLSocket.html
好吧,API 16
(4.1
)就提供支持,但API 20
(5.0
)才默认打开。。。
那能不能手动打开呢,先看一下执行结果:
5.0
:在初始化的ssLSocketFactory
的时候,sslParameters
里面enabledProtocls
列表包含了TLSv1.2
:
4.4
:只有TLSv1
:
再看初始化sslParameters
的源码:
5.0
下:
com.android.org.conscrypt.SSLParametersImpl.java
:
protected SSLParametersImpl(KeyManager[] kms, TrustManager[] tms, SecureRandom sr, ClientSessionContext clientSessionContext, ServerSessionContext serverSessionContext) throws KeyManagementException {
this.serverSessionContext = serverSessionContext;
this.clientSessionContext = clientSessionContext;
// initialize key managers
if (kms == null) {
x509KeyManager = getDefaultX509KeyManager();
// There's no default PSK key manager
pskKeyManager = null;
} else {
x509KeyManager = findFirstX509KeyManager(kms);
pskKeyManager = findFirstPSKKeyManager(kms);
}
// initialize x509TrustManager
if (tms == null) {
x509TrustManager = getDefaultX509TrustManager();
} else {
x509TrustManager = findFirstX509TrustManager(tms);
}
// initialize secure random
// We simply use the SecureRandom passed in by the caller. If it's
// null, we don't replace it by a new instance. The native code below
// then directly accesses /dev/urandom. Not the most elegant solution,
// but faster than going through the SecureRandom object.
secureRandom = sr;
// initialize the list of cipher suites and protocols enabled by default
enabledProtocols = getDefaultProtocols();
boolean x509CipherSuitesNeeded = (x509KeyManager != null) || (x509TrustManager != null);
boolean pskCipherSuitesNeeded = pskKeyManager != null;
enabledCipherSuites = getDefaultCipherSuites(x509CipherSuitesNeeded, pskCipherSuitesNeeded);
}
private static String[] getDefaultProtocols() {
return NativeCrypto.DEFAULT_PROTOCOLS.clone();
}
com.android.org.conscrypt. NativeCrypto.java
:
public static final String[] DEFAULT_PROTOCOLS = new String[] {
SUPPORTED_PROTOCOL_SSLV3,
SUPPORTED_PROTOCOL_TLSV1,
SUPPORTED_PROTOCOL_TLSV1_1,
SUPPORTED_PROTOCOL_TLSV1_2,
};
4.4
下:
com.android.org.conscrypt.SSLParametersImpl.java
:
// protocols available for SSL connection
private String[] enabledProtocols = ProtocolVersion.supportedProtocols;
com.android.org.conscrypt. ProtocolVersion.java
:
/**
* Protocols supported by this provider implementation
*/
public static final String[] supportedProtocols = new String[] { "TLSv1", "SSLv3" };
所以不管是4.4
还是5.0
,支持的TLS
版本都被写死在了外部扩展库conscrypt.jar
里面。
所以看来需要重新编译Android 4.4
的源码才能解决了。。。