一、问题描述
最近将http向https迁移过程中遇到一个问题,当我使用HttpURLConnection发送了一个Https网络请求时候,返回网络状态码为3xx。大家都知道这个状态码表示重定向,理论上客户端会重新发起一个重定向的请求,但通过抓包分析(Https如何抓包的问题,相信聪明的你可以搞定),发现客户端并没有重新发起请求从而导致App故障。当我切回到Http的时候故障就消失了。
二、问题分析
根据上边的问题,我们先通过Fidder去抓包,然后分析包里边的内容来定位。
- 根据抓包,我们得知第一个请求无论是https还是http,返回状态码都是一样的307。说明这个接口的服务地址是没有问题的。
- 查看重定向地址,第一次无论是https还是http,返回的重定向地址是一样的,都是http,该重定向地址在浏览器里边访问后,返回状态码是200。
从上边两点可以得出服务后端的接口是没有问题的,后来就查询http协议文档,文档也没有说明https不能重定向http或者http重定向https。然后就怀疑HttpURLConnection这个请求工具有问题,如何找出问题呢?源码
三、问题探索
首先从发起网络端开始:
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
url是一个URL类型,查看他的OpenConnection()方法:
public final class URL implements java.io.Serializable {
/**
* The URLStreamHandler for this URL.
*/
transient URLStreamHandler handler;
.......//省略n行代码
public URLConnection openConnection() throws java.io.IOException {
return handler.openConnection(this);
}
}
我们发现执行openConnection()操作交给了URLStreamHandler handler这个变量,我们看下这个变量如何初始化的:
public final class URL implements java.io.Serializable {
.......//省略n行代码
public URL(URL context, String spec, URLStreamHandler handler)
throws MalformedURLException
{
String original = spec;
int i, limit, c;
int start = 0;
String newProtocol = null;
boolean aRef=false;
boolean isRelative = false;
// Check for permission to specify a handler
if (handler != null) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkSpecifyHandler(sm);
}
}
try {
limit = spec.length();
while ((limit > 0) && (spec.charAt(limit - 1) <= ' ')) {
limit--; //eliminate trailing whitespace
}
while ((start < limit) && (spec.charAt(start) <= ' ')) {
start++; // eliminate leading whitespace
}
if (spec.regionMatches(true, start, "url:", 0, 4)) {
start += 4;
}
if (start < spec.length() && spec.charAt(start) == '#') {
/* we're assuming this is a ref relative to the context URL.
* This means protocols cannot start w/ '#', but we must parse
* ref URL's like: "hello:there" w/ a ':' in them.
*/
aRef=true;
}
for (i = start ; !aRef && (i < limit) &&
((c = spec.charAt(i)) != '/') ; i++) {
if (c == ':') {
String s = spec.substring(start, i).toLowerCase();
if (isValidProtocol(s)) {
newProtocol = s;
start = i + 1;
}
break;
}
}
// Only use our context if the protocols match.
protocol = newProtocol; //根据我们的需求 我们的newProtocol 内容是一个https的字串
if ((context != null) && ((newProtocol == null) ||
newProtocol.equalsIgnoreCase(context.protocol))) {
// inherit the protocol handler from the context
// if not specified to the constructor
if (handler == null) {
handler = context.handler;
}
// If the context is a hierarchical URL scheme and the spec
// contains a matching scheme then maintain backwards
// compatibility and treat it as if the spec didn't contain
// the scheme; see 5.2.3 of RFC2396
if (context.path != null && context.path.startsWith("/"))
newProtocol = null;
if (newProtocol == null) {
protocol = context.protocol;
authority = context.authority;
userInfo = context.userInfo;
host = context.host;
port = context.port;
file = context.file;
path = context.path;
isRelative = true;
}
}
if (protocol == null) {
throw new MalformedURLException("no protocol: "+original);
}
// Get the protocol handler if not specified or the protocol
// of the context could not be used
if (handler == null &&
(handler = getURLStreamHandler(protocol)) == null) {//本地变量handler初始化 然后复制给全局handler 该方法在下边
throw new MalformedURLException("unknown protocol: "+protocol);
}
this.handler = handler; // handler 在这一行进行初始化
i = spec.indexOf('#', start);
if (i >= 0) {
ref = spec.substring(i + 1, limit);
limit = i;
}
/*
* Handle special case inheritance of query and fragment
* implied by RFC2396 section 5.2.2.
*/
if (isRelative && start == limit) {
query = context.query;
if (ref == null) {
ref = context.ref;
}
}
handler.parseURL(this, spec, start, limit);
} catch(MalformedURLException e) {
throw e;
} catch(Exception e) {
MalformedURLException exception = new MalformedURLException(e.getMessage());
exception.initCause(e);
throw exception;
}
}
...//省略n行代码
static URLStreamHandler getURLStreamHandler(String protocol) {
URLStreamHandler handler = (URLStreamHandler)handlers.get(protocol);//一个Map缓存 先走缓存取
if (handler == null) { //没有命中情况下
boolean checkedWithFactory = false;
// Use the factory (if any)
if (factory != null) { //factory == null
handler = factory.createURLStreamHandler(protocol);
checkedWithFactory = true;
}
// Try java protocol handler
if (handler == null) {//尝试java协议的handler
final String packagePrefixList = System.getProperty(protocolPathProp,"");
StringTokenizer packagePrefixIter = new StringTokenizer(packagePrefixList, "|");
while (handler == null &&
packagePrefixIter.hasMoreTokens()) {
String packagePrefix = packagePrefixIter.nextToken().trim();
try {
String clsName = packagePrefix + "." + protocol + ".Handler";
Class cls = null;
try {
ClassLoader cl = ClassLoader.getSystemClassLoader();
cls = Class.forName(clsName, true, cl);
} catch (ClassNotFoundException e) {
ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
if (contextLoader != null) {
cls = Class.forName(clsName, true, contextLoader);
}
}
if (cls != null) {
handler =
(URLStreamHandler)cls.newInstance();
}
} catch (ReflectiveOperationException ignored) {
}
}
}
// Fallback to built-in stream handler.
// Makes okhttp the default http/https handler
if (handler == null) {//在Android平台 handler == null 应该会走到改语句块内
try {
if (protocol.equals("file")) {
handler = (URLStreamHandler)Class.
forName("sun.net.www.protocol.file.Handler").newInstance();
} else if (protocol.equals("ftp")) {
handler = (URLStreamHandler)Class.
forName("sun.net.www.protocol.ftp.Handler").newInstance();
} else if (protocol.equals("jar")) {
handler = (URLStreamHandler)Class.
forName("sun.net.www.protocol.jar.Handler").newInstance();
} else if (protocol.equals("http")) {
handler = (URLStreamHandler)Class.
forName("com.android.okhttp.HttpHandler").newInstance();
} else if (protocol.equals("https")) {//我们的需求 进入https
handler = (URLStreamHandler)Class.
forName("com.android.okhttp.HttpsHandler").newInstance();//反射生成com.android.okhttp.HttpsHandler实例
}
} catch (Exception e) {
throw new AssertionError(e);
}
}
synchronized (streamHandlerLock) {
URLStreamHandler handler2 = null;
// Check again with hashtable just in case another
// thread created a handler since we last checked
handler2 = (URLStreamHandler)handlers.get(protocol);
if (handler2 != null) {
return handler2;
}
// Check with factory if another thread set a
// factory since our last check
if (!checkedWithFactory && factory != null) {
handler2 = factory.createURLStreamHandler(protocol);
}
if (handler2 != null) {
// The handler from the factory must be given more
// importance. Discard the default handler that
// this thread created.
handler = handler2;
}
// Insert this handler into the hashtable
if (handler != null) {
handlers.put(protocol, handler);//缓存该实例
}
}
}
return handler;// 返回实例
}
}
通过上述代码,我们发现会通过反射去生成一个com.android.okhttp.HttpsHandler实例,按照该路径我在Andorid 源码里边查找该类,结果没找到,路线再次没有了。
然后google了一下,从回答中探索答案,发现HttpURLConnection从Android 5.0后其http和https是使用okHttp实现的,引用作者的回答:
I can not find *com.android.okhttp.internal.http.HttpURLConnectionImpl * in android aosp5.11 (http://androidxref.com/),
but find *com.squareup.okhttp.internal.huc.HttpURLConnectionImpl *
OkHttp in Android is here: [https://android.googlesource.com/platform/external/okhttp/+/master]
(https://android.googlesource.com/platform/external/okhttp/+/master). The reason the package name iscom.android.okhttp
is because there are jarjar rules which repackage it under that name.
根据上边英文,我们知道Android 重新导出了新的jar,jar中将 com.squareup这两级目录改成com.android。所以我们直接找com.squareup.okhttp.HttpsHandler,在源码中果真找到了改java文件:
package com.squareup.okhttp;
import java.net.Proxy;
import java.util.Collections;
import java.util.List;
import javax.net.ssl.HttpsURLConnection;
public final class HttpsHandler extends HttpHandler {
/**
* The connection spec to use when connecting to an https:// server. Note that Android does
* not set the cipher suites or TLS versions to use so the socket's defaults will be used
* instead. When the SSLSocketFactory is provided by the app or GMS core we will not
* override the enabled ciphers or TLS versions set on the sockets it produces with a
* list hardcoded at release time. This is deliberate.
*/
private static final ConnectionSpec TLS_CONNECTION_SPEC = ConnectionSpecs.builder(true)
.allEnabledCipherSuites()
.allEnabledTlsVersions()
.supportsTlsExtensions(true)
.build();
private static final List HTTP_1_1_ONLY =
Collections.singletonList(Protocol.HTTP_1_1);
private final ConfigAwareConnectionPool configAwareConnectionPool =
ConfigAwareConnectionPool.getInstance();
@Override protected int getDefaultPort() {
return 443;
}
@Override
protected OkUrlFactory newOkUrlFactory(Proxy proxy) {
OkUrlFactory okUrlFactory = createHttpsOkUrlFactory(proxy);
// For HttpsURLConnections created through java.net.URL Android uses a connection pool that
// is aware when the default network changes so that pooled connections are not re-used when
// the default network changes.
okUrlFactory.client().setConnectionPool(configAwareConnectionPool.get());
return okUrlFactory;
}
/**
* Creates an OkHttpClient suitable for creating {@link HttpsURLConnection} instances on
* Android.
*/
// Visible for android.net.Network.
public static OkUrlFactory createHttpsOkUrlFactory(Proxy proxy) {
// The HTTPS OkHttpClient is an HTTP OkHttpClient with extra configuration.
OkUrlFactory okUrlFactory = HttpHandler.createHttpOkUrlFactory(proxy);
// All HTTPS requests are allowed.
OkUrlFactories.setUrlFilter(okUrlFactory, null);
OkHttpClient okHttpClient = okUrlFactory.client();
// Only enable HTTP/1.1 (implies HTTP/1.0). Disable SPDY / HTTP/2.0.
okHttpClient.setProtocols(HTTP_1_1_ONLY);
okHttpClient.setConnectionSpecs(Collections.singletonList(TLS_CONNECTION_SPEC));
// Android support certificate pinning via NetworkSecurityConfig so there is no need to
// also expose OkHttp's mechanism. The OkHttpClient underlying https HttpsURLConnections
// in Android should therefore always use the default certificate pinner, whose set of
// {@code hostNamesToPin} is empty.
okHttpClient.setCertificatePinner(CertificatePinner.DEFAULT);
// OkHttp does not automatically honor the system-wide HostnameVerifier set with
// HttpsURLConnection.setDefaultHostnameVerifier().
okUrlFactory.client().setHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier());
// OkHttp does not automatically honor the system-wide SSLSocketFactory set with
// HttpsURLConnection.setDefaultSSLSocketFactory().
// See https://github.com/square/okhttp/issues/184 for details.
okHttpClient.setSslSocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory());
return okUrlFactory;
}
}
我们发现它继承自HttpHandler,查看HttpHandler源码如下:
package com.squareup.okhttp;
import com.squareup.okhttp.internal.URLFilter;
import libcore.net.NetworkSecurityPolicy;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.ResponseCache;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class HttpHandler extends URLStreamHandler {
private final static List CLEARTEXT_ONLY =
Collections.singletonList(ConnectionSpec.CLEARTEXT);
private static final CleartextURLFilter CLEARTEXT_FILTER = new CleartextURLFilter();
private final ConfigAwareConnectionPool configAwareConnectionPool =
ConfigAwareConnectionPool.getInstance();
@Override protected URLConnection openConnection(URL url) throws IOException {
return newOkUrlFactory(null /* proxy */).open(url);
}
@Override protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
if (url == null || proxy == null) {
throw new IllegalArgumentException("url == null || proxy == null");
}
return newOkUrlFactory(proxy).open(url);
}
@Override protected int getDefaultPort() {
return 80;
}
protected OkUrlFactory newOkUrlFactory(Proxy proxy) {
OkUrlFactory okUrlFactory = createHttpOkUrlFactory(proxy);
// For HttpURLConnections created through java.net.URL Android uses a connection pool that
// is aware when the default network changes so that pooled connections are not re-used when
// the default network changes.
okUrlFactory.client().setConnectionPool(configAwareConnectionPool.get());
return okUrlFactory;
}
/**
* Creates an OkHttpClient suitable for creating {@link java.net.HttpURLConnection} instances on
* Android.
*/
// Visible for android.net.Network.
public static OkUrlFactory createHttpOkUrlFactory(Proxy proxy) {
OkHttpClient client = new OkHttpClient();
// Explicitly set the timeouts to infinity.
client.setConnectTimeout(0, TimeUnit.MILLISECONDS);
client.setReadTimeout(0, TimeUnit.MILLISECONDS);
client.setWriteTimeout(0, TimeUnit.MILLISECONDS);
// Set the default (same protocol) redirect behavior. The default can be overridden for
// each instance using HttpURLConnection.setInstanceFollowRedirects().
client.setFollowRedirects(HttpURLConnection.getFollowRedirects());
// Do not permit http -> https and https -> http redirects.
client.setFollowSslRedirects(false);
// Permit cleartext traffic only (this is a handler for HTTP, not for HTTPS).
client.setConnectionSpecs(CLEARTEXT_ONLY);
// When we do not set the Proxy explicitly OkHttp picks up a ProxySelector using
// ProxySelector.getDefault().
if (proxy != null) {
client.setProxy(proxy);
}
// OkHttp requires that we explicitly set the response cache.
OkUrlFactory okUrlFactory = new OkUrlFactory(client);
// Use the installed NetworkSecurityPolicy to determine which requests are permitted over
// http.
OkUrlFactories.setUrlFilter(okUrlFactory, CLEARTEXT_FILTER);
ResponseCache responseCache = ResponseCache.getDefault();
if (responseCache != null) {
AndroidInternal.setResponseCache(okUrlFactory, responseCache);
}
return okUrlFactory;
}
private static final class CleartextURLFilter implements URLFilter {
@Override
public void checkURLPermitted(URL url) throws IOException {
String host = url.getHost();
if (!NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(host)) {
throw new IOException("Cleartext HTTP traffic to " + host + " not permitted");
}
}
}
}
通过上边两个java文件我们发现在执行openConnectionpublic()方法过程中,在 HttpsHandler会执行到
public final class HttpsHandler extends HttpHandler {
......
public static OkUrlFactory createHttpsOkUrlFactory(Proxy proxy) {
// The HTTPS OkHttpClient is an HTTP OkHttpClient with extra configuration.
OkUrlFactory okUrlFactory = HttpHandler.createHttpOkUrlFactory(proxy);
......
}
}
public class HttpHandler extends URLStreamHandler {
// Visible for android.net.Network.
public static OkUrlFactory createHttpOkUrlFactory(Proxy proxy) {
OkHttpClient client = new OkHttpClient();
......
// Do not permit http -> https and https -> http redirects.//这行代码的注释说明了一切
client.setFollowSslRedirects(false);
......
}
}
通过HttpHandler.createHttpOkUrlFactory(proxy)方法中的注解我们知道HttpURLConnection请求过程中,https不能重定向到http,相反,http也不能重定向到https也成立。
分析到这这个问题就明朗了,这应该算是HttpURLConnection中的一个bug吧,毕竟在http协议中并没有这么规定,希望大家后边在用到该网络工具时候注意下该问题。
感谢你的耐心阅读,如有错误,欢迎指正。如果本文对你有帮助,记得点赞。欢迎关注我的微信公众号。