OkHttp
okhttp是Android 平台的网络请求框架,已经被Android吸收了。并且OkHttp很好的支持了Cookie,Cache,连接复用,HTTP2 Https...使用拦截链模式,能很方便的扩展自己的功能。从Android4.4开始已经引入了OkHttp
ConnectionSpec
ConnectionSpec是用来指定Http传输时的Socket连接。对于Https连接,ConnectionSpec是在构建TLS连接时向服务端说明客户端支持的TLS版本,密码套件等的一个类。
下面介绍一下OkHttp默认的ConnectionSpec
// Most secure but generally supported list. 支持的密码套件的列表
private static final CipherSuite[] RESTRICTED_CIPHER_SUITES = new CipherSuite[] {
// TLSv1.3
CipherSuite.TLS_AES_128_GCM_SHA256,
CipherSuite.TLS_AES_256_GCM_SHA384,
CipherSuite.TLS_CHACHA20_POLY1305_SHA256,
CipherSuite.TLS_AES_128_CCM_SHA256,
CipherSuite.TLS_AES_256_CCM_8_SHA256,
CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
};
//支持的密码套件的列表
// This is nearly equal to the cipher suites supported in Chrome 51, current as of 2016-05-25.
// All of these suites are available on Android 7.0; earlier releases support a subset of these
// suites. https://github.com/square/okhttp/issues/1972
private static final CipherSuite[] APPROVED_CIPHER_SUITES = new CipherSuite[] {
// TLSv1.3
CipherSuite.TLS_AES_128_GCM_SHA256,
CipherSuite.TLS_AES_256_GCM_SHA384,
CipherSuite.TLS_CHACHA20_POLY1305_SHA256,
CipherSuite.TLS_AES_128_CCM_SHA256,
CipherSuite.TLS_AES_256_CCM_8_SHA256,
CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
// Note that the following cipher suites are all on HTTP/2's bad cipher suites list. We'll
// continue to include them until better suites are commonly available. For example, none
// of the better cipher suites listed above shipped with Android 4.4 or Java 7.
CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384,
CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,
CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,
CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
};
//安全的TLS连接需要最近的客户端和最近的服务器
public static final ConnectionSpec RESTRICTED_TLS = new Builder(true)
.cipherSuites(RESTRICTED_CIPHER_SUITES)
.tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2)
.supportsTlsExtensions(true)
.build();
//现代的TLS的配置,适用于大多数客户端和可以连接到的服务器,是OkHttp的默认配置
public static final ConnectionSpec MODERN_TLS = new Builder(true)
.cipherSuites(APPROVED_CIPHER_SUITES)
.tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0)
.supportsTlsExtensions(true)
.build();
//向后兼容的配置,相比于MODERN_TLS,支持的TLS的版本变少了,只支持TLS_1_0版本
public static final ConnectionSpec COMPATIBLE_TLS = new Builder(true)
.cipherSuites(APPROVED_CIPHER_SUITES)
.tlsVersions(TlsVersion.TLS_1_0)
.supportsTlsExtensions(true)
.build();
//未加密未认证的连接,就是HTTP连接
public static final ConnectionSpec CLEARTEXT = new Builder(false).build();
上面是OkHttp提供的一些默认的ConnectionSpec,再找下OKHttpClient中默认用的是哪些ConnectionSpec
static final List<ConnectionSpec> DEFAULT_CONNECTION_SPECS = Util.immutableList(
ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);
public Builder() {
......
protocols = DEFAULT_PROTOCOLS;
connectionSpecs = DEFAULT_CONNECTION_SPECS;
...
}
从代码中看的出来,默认ConnectionSpec.MODERN_TLS(支持较多的TLS版本和较多的密码套件), ConnectionSpec.CLEARTEXT(支持HTTP连接)
下面再拆解ConnectionSpec
final boolean tls;//是否是TLS连接
final boolean supportsTlsExtensions;//是否支持Tls扩展
final @Nullable String[] cipherSuites;//支持的密码套件类型
final @Nullable String[] tlsVersions;//支持的Tls版本的类型
//把相应的ConnectionSpec设置到SSLSocket上面,使其具有相应的密码套件和TLS版本的支持
//isFallback 是否支持回退策略,就是是否还支持其他的密码套件
void apply(SSLSocket sslSocket, boolean isFallback) {
ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);//返回这个socket支持的ConnectionSpec,并在设置到SSLSocket上
if (specToApply.tlsVersions != null) {
sslSocket.setEnabledProtocols(specToApply.tlsVersions);
}
if (specToApply.cipherSuites != null) {
sslSocket.setEnabledCipherSuites(specToApply.cipherSuites);
}
}
//返回sslSocket支持的密码套件和TLS版本
private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) {
String[] cipherSuitesIntersection = cipherSuites != null
? intersect(CipherSuite.ORDER_BY_NAME, sslSocket.getEnabledCipherSuites(), cipherSuites)
: sslSocket.getEnabledCipherSuites();
String[] tlsVersionsIntersection = tlsVersions != null
? intersect(Util.NATURAL_ORDER, sslSocket.getEnabledProtocols(), tlsVersions)
: sslSocket.getEnabledProtocols();
// In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
// the SCSV cipher is added to signal that a protocol fallback has taken place.
String[] supportedCipherSuites = sslSocket.getSupportedCipherSuites();
int indexOfFallbackScsv = indexOf(
CipherSuite.ORDER_BY_NAME, supportedCipherSuites, "TLS_FALLBACK_SCSV");
if (isFallback && indexOfFallbackScsv != -1) {
cipherSuitesIntersection = concat(
cipherSuitesIntersection, supportedCipherSuites[indexOfFallbackScsv]);
}
return new Builder(this)
.cipherSuites(cipherSuitesIntersection)
.tlsVersions(tlsVersionsIntersection)
.build();//构造出ConnectionSpec
}
//是否与传入的SSLSocket具有匹配的密码套件和TLS版本
public boolean isCompatible(SSLSocket socket) {
if (!tls) {
return false;
}
if (tlsVersions != null && !nonEmptyIntersection(
Util.NATURAL_ORDER, tlsVersions, socket.getEnabledProtocols())) {
return false;
}
if (cipherSuites != null && !nonEmptyIntersection(
CipherSuite.ORDER_BY_NAME, cipherSuites, socket.getEnabledCipherSuites())) {
return false;
}
return true;
}
调用Apply方法的入口地址是在RealConnection的connectTls()方法中
确定这个socket简历TLS连接时的对应的密码套件和TLS版本
// Configure the socket's ciphers, TLS versions, and extensions.
ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
ConnectionSpecSelector
ConnectionSpecSelector是处理ConnectionSpec的后备策略,当Https由于握手或者TLS版本,密码套件问题连接失败时,可以尝试使用不同的协议。ConnectionSpecSelector是有状态的,所以对于每一个Connection都应该创建一个新的ConnectionSpecSelector
private final List<ConnectionSpec> connectionSpecs;//支持的ConnectionSpec列表
private int nextModeIndex;//当前可用的协议在列表中的位置的下一个索引(便于查找是否支持后备策略)
private boolean isFallbackPossible;//是否支持后备策略
private boolean isFallback;//是否处于后备策略
ConnectionSpecSelector比较简单里面的方法有
public ConnectionSpecSelector(List<ConnectionSpec> connectionSpecs) {
this.nextModeIndex = 0;
this.connectionSpecs = connectionSpecs;
}
public ConnectionSpec configureSecureSocket(SSLSocket sslSocket) throws IOException {
ConnectionSpec tlsConfiguration = null;
for (int i = nextModeIndex, size = connectionSpecs.size(); i < size; i++) {
ConnectionSpec connectionSpec = connectionSpecs.get(i);
if (connectionSpec.isCompatible(sslSocket)) {
tlsConfiguration = connectionSpec;
nextModeIndex = i + 1;
break;
}
}
if (tlsConfiguration == null) {
// This may be the first time a connection has been attempted and the socket does not support
// any the required protocols, or it may be a retry (but this socket supports fewer
// protocols than was suggested by a prior socket).
throw new UnknownServiceException(
"Unable to find acceptable protocols. isFallback=" + isFallback
+ ", modes=" + connectionSpecs
+ ", supported protocols=" + Arrays.toString(sslSocket.getEnabledProtocols()));
}
isFallbackPossible = isFallbackPossible(sslSocket);
Internal.instance.apply(tlsConfiguration, sslSocket, isFallback);
return tlsConfiguration;
}
//是否支持后备策略,从nextModeIndex处开始遍历,判断后面的ConnectionSpec是否匹配SSLSocket
private boolean isFallbackPossible(SSLSocket socket) {
for (int i = nextModeIndex; i < connectionSpecs.size(); i++) {
if (connectionSpecs.get(i).isCompatible(socket)) {
return true;
}
}
return false;
}
- 构造方法中nextModeIndex为0,遍历connectionSpec列表,找到适合SSLSocket的ConnectionSpec,如果未找到直接抛出异常
- 判断在ConnectionSpec列表的后面是否还有适合SSLSocket的connectionSpec,如果有的话,那么就代表支持支持后备策略isFallbackPossible为true
- 将支持的ConnectionSpec设置给SSLSocket对象,并且此时isFallback还是为isFallback,表明目前还未处于后备策略中
public boolean connectionFailed(IOException e) {
// Any future attempt to connect using this strategy will be a fallback attempt.
isFallback = true;
if (!isFallbackPossible) {
return false;
}
// If there was a protocol problem, don't recover.
if (e instanceof ProtocolException) {
return false;
}
// If there was an interruption or timeout (SocketTimeoutException), don't recover.
// For the socket connect timeout case we do not try the same host with a different
// ConnectionSpec: we assume it is unreachable.
if (e instanceof InterruptedIOException) {
return false;
}
// Look for known client-side or negotiation errors that are unlikely to be fixed by trying
// again with a different connection spec.
if (e instanceof SSLHandshakeException) {
// If the problem was a CertificateException from the X509TrustManager,
// do not retry.
if (e.getCause() instanceof CertificateException) {
return false;
}
}
if (e instanceof SSLPeerUnverifiedException) {
// e.g. a certificate pinning error.
return false;
}
// On Android, SSLProtocolExceptions can be caused by TLS_FALLBACK_SCSV failures, which means we
// retry those when we probably should not.
return (e instanceof SSLHandshakeException
|| e instanceof SSLProtocolException
|| e instanceof SSLException);
}
- 如果之前的SSLSocket连接失败了,那么之后连接尝试都属于后备策略,所以isFallBack置为true
上面是分别介绍了ConnectionSpec和ConnectionSpecSelector,下面把这两个类直接串联起来,看看OkHttp里面是怎么使用的
在RealConnection中
public void connect(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
EventListener eventListener) {
...
List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);//
if (route.address().sslSocketFactory() == null) {
if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication not enabled for client"));
}
String host = route.address().url().host();
if (!Platform.get().isCleartextTrafficPermitted(host)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication to " + host + " not permitted by network security policy"));
}
} else {
if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
throw new RouteException(new UnknownServiceException(
"H2_PRIOR_KNOWLEDGE cannot be used with HTTPS"));
}
}
while (true) {
try {
if (route.requiresTunnel()) {
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
if (rawSocket == null) {
// We were unable to connect the tunnel but properly closed down our resources.
break;
}
} else {
connectSocket(connectTimeout, readTimeout, call, eventListener);
}
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
break;
} catch (IOException e) {
...
//这里的connectionRetryEnabled默认值为true,是从OKHttpClient中传过来的,默认值为true
//所以这里会执行ConnectionSpecSelector的connectionFailed()方法,如果是SSLHandshakeException,SSLProtocolException,SSLException那么就会执行后备策略,继续尝试TLS连接
if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
throw routeException;
}
}
}
...
}
- Address里面的ConnectionSpec就是OkHttpClient传入的ConnectionSpec,也就是之前说到的默认值
- 通过ConnectionSpecs构造出ConnectionSpecSelector
- 对于HTTP连接,如果OkHttp和Platform不支持Http的话,那么就抛出异常,Platform在Android
上是Android Platform - 开始尝试连接了,connectSocket主要是构建TCP连接,establishProtocol方法里面会构建TLS连接,最终会走到connectTls方法
- 如果连接过程失败了,那么会调用connectionSpecSelector的connectionFailed(),如果异常是SSLHandshakeException,SSLProtocolException,SSLException那么就会执行后备策略,继续尝试TLS连接,走回while(true)
下面再具体分析connectTls()方法,开始Tls连接
private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
Address address = route.address();
SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
boolean success = false;
SSLSocket sslSocket = null;
try {
// Create the wrapper over the connected socket.
sslSocket = (SSLSocket) sslSocketFactory.createSocket(
rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
// Configure the socket's ciphers, TLS versions, and extensions.
ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
if (connectionSpec.supportsTlsExtensions()) {
Platform.get().configureTlsExtensions(
sslSocket, address.url().host(), address.protocols());
}
// Force handshake. This can throw!
sslSocket.startHandshake();
// block for session establishment
SSLSession sslSocketSession = sslSocket.getSession();
Handshake unverifiedHandshake = Handshake.get(sslSocketSession);
// Verify that the socket's certificates are acceptable for the target host.
if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) {
X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:"
+ "\n certificate: " + CertificatePinner.pin(cert)
+ "\n DN: " + cert.getSubjectDN().getName()
+ "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
}
// Check that the certificate pinner is satisfied by the certificates presented.
address.certificatePinner().check(address.url().host(),
unverifiedHandshake.peerCertificates());
// Success! Save the handshake and the ALPN protocol.
String maybeProtocol = connectionSpec.supportsTlsExtensions()
? Platform.get().getSelectedProtocol(sslSocket)
: null;
socket = sslSocket;
source = Okio.buffer(Okio.source(socket));
sink = Okio.buffer(Okio.sink(socket));
handshake = unverifiedHandshake;
protocol = maybeProtocol != null
? Protocol.get(maybeProtocol)
: Protocol.HTTP_1_1;
success = true;
} catch (AssertionError e) {
if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
} finally {
if (sslSocket != null) {
Platform.get().afterHandshake(sslSocket);
}
if (!success) {
closeQuietly(sslSocket);
}
}
}
- connectionSpecSelector.configureSecureSocket(sslSocket);开始确认密码套件,Tls版本和tls扩展等,这里就会调用ConnectionSpecSelector的方法,这里就是入口,这样就串联起来了,可以从这里进入,再结合上面对两个类的分析,再梳理下流程
网友评论