Flutter 跨端网络抓包 (以Android 为例)

4,058 阅读6分钟

背景

在很多公司测试环境使用的是内网测试,我们公司也是。
但是我们有点扯的是内网的域名没有配置内网域名解析,必须手动配置hosts才可以正常访问测试环境的域名。 如下:

# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1	localhost
255.255.255.255	broadcasthost
::1             localhost

# 公司内部域名
10.11.x.x   http://www.xxxx.com

所以在测试环境时,操作步骤一般是:在电脑上配置hosts后,然后手机链接电脑上的代理,在进行测试。

在我们之前没有使用Flutter技术的时候,手机链接电脑上的代理,啥问题都没有。电脑上装一个charles

然后在手机上链接电脑的IP

但是我们使用了Flutter技术后,发现这一套不好使了,下面进入今天的主题,分析一下这样做为什么抓不到包的原因以及解决方法。

原因分析

我们知道要科学上网访问国外的一些资源需要设置代理服务器,那么下面 以Android的Java为例,讲解代理.

Java代理

Java 中代理主要的两个类Proxy和ProxySelector,先看一下 Proxy

public class Proxy {

    /**
     * Represents the proxy type.
     *
     * @since 1.5
     */
    public enum Type {
        /**
         * Represents a direct connection, or the absence of a proxy.
         */
        DIRECT,
        /**
         * Represents proxy for high level protocols such as HTTP or FTP.
         */
        HTTP,
        /**
         * Represents a SOCKS (V4 or V5) proxy.
         */
        SOCKS
    };

    private Type type;
    private SocketAddress sa;

    /**
     * A proxy setting that represents a {@code DIRECT} connection,
     * basically telling the protocol handler not to use any proxying.
     * Used, for instance, to create sockets bypassing any other global
     * proxy settings (like SOCKS):
     * <P>
     * {@code Socket s = new Socket(Proxy.NO_PROXY);}
     *
     */
    public final static Proxy NO_PROXY = new Proxy();

    // Creates the proxy that represents a {@code DIRECT} connection.
    private Proxy() {
        type = Type.DIRECT;
        sa = null;
    }
    ......

看代码我们可以知道,代理一般分为DIRECT(也被称为没有代理 NO_PROXY),HTTP代理(高级协议代理,HTTP、FTP等的代理),SOCKS 代理.

这个类怎么用呢?以获取百度网页为例,设置URLConnection的代理

    private final String PROXY_ADDR = "xxx.xxx.xxx.xxx";
    private final int PROXY_PORT = 10086;

    public void readHtml() throws IOException {
        URL url = new URL("http://www.baidu.com"); 
        Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(PROXY_ADDR, PROXY_PORT));
        //链接时,使用代理链接
        URLConnection conn = url.openConnection(proxy); 
        conn.setConnectTimeout(3000);

        InputStream inputStream = conn.getInputStream();
        //获得输入流,开始读入....
    }

目前大多数Java都使用OkHttp作为网络请求访问,OkHttp的代理设置更为简单

public void readHtml() {
    Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(PROXY_ADDR, PROXY_PORT));
    OkHttpClient okHttpClient = new OkHttpClient.Builder().proxy(proxy).build();
}

我们的应用就是在release环境下,禁用了抓包(包含http不需要证书的抓包),设置如下:

 public void init() {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        if(!BuildConfig.DEBUG) {
            builder.proxy(Proxy.NO_PROXY);
        }
        OkHttpClient okHttpClient = builder.build();
    }

追看一下OkHttp源码,可以知道,http的链接最终调用到了RealConnection. 链接最终调用到了connectSocket

 /** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
  private void connectSocket(int connectTimeout, int readTimeout, Call call,
      EventListener eventListener) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();
    
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

看这个函数Okhttp的注释,可以知道,Okhttp是基于socket去实现的一套完整的http协议,无论是那种代理方式,都返回的是一个socket链接对象,我们看一下new Socket的实现

 public Socket(Proxy proxy) {
      ....
        Proxy p = proxy == Proxy.NO_PROXY ? Proxy.NO_PROXY
                                          : sun.net.ApplicationProxy.create(proxy);
        Proxy.Type type = p.type();
        // if (type == Proxy.Type.SOCKS || type == Proxy.Type.HTTP) {
        if (type == Proxy.Type.SOCKS) {
            impl = new SocksSocketImpl(p);
            impl.setSocket(this);
        } else {
            if (p == Proxy.NO_PROXY) {
                if (factory == null) {
                    impl = new PlainSocketImpl();
                    impl.setSocket(this);
                } else
                    setImpl();
            } else
                throw new IllegalArgumentException("Invalid Proxy");
        }
    }

如果有代理,最终调用到了SocksSocketImpl(p)

   SocksSocketImpl(Proxy proxy) {
        SocketAddress a = proxy.address();
        if (a instanceof InetSocketAddress) {
            InetSocketAddress ad = (InetSocketAddress) a;
            // Use getHostString() to avoid reverse lookups
            server = ad.getHostString();
            serverPort = ad.getPort();
        }
    }

通过代理的host和端口去链接服务. 那发现了其中原理,那么他是怎么读取系统设置的代理呢?下面来看一下ProxySelector

ProxySelector

直接显示传入Porxy对象的方法未免有点太繁琐,并且无法直接读取系统所设置的代理,Java提供了一个抽象类ProxySelector,该类的对象可以根据你要连接的URL自动选择最合适的代理,但是该类是抽象类。看一下:

public abstract class ProxySelector {
    private static ProxySelector theProxySelector;
    .......
     public static void setDefault(ProxySelector ps) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(SecurityConstants.SET_PROXYSELECTOR_PERMISSION);
        }
        theProxySelector = ps;
    }
    
    public abstract List<Proxy> select(URI uri);
    public abstract void connectFailed(URI uri, SocketAddress sa, IOException ioe);
}

实现一个自己的代理选择器

public void init() {
        ProxySelector.setDefault(new ProxySelector() {
            @Override
            public List<Proxy> select(URI uri) { 
                List<Proxy> list = new ArrayList<>();
                list.add(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(PROXY_ADDR, PROXY_PORT)));
                return list;
            }

            @Override
            public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { 
              //链接失败
            }

        });
    }

OkHttp初始化的时候,可以直接设置,也可以获取默认的

public Builder proxySelector(ProxySelector proxySelector) {
      this.proxySelector = proxySelector;
      return this;
}

public Builder() {
    ·····
    proxySelector = ProxySelector.getDefault();
    ·····
}

自动选择我们设置的代理,那么还是有问题,怎么读取手机上设置的代理呢,看一下ProxySelector我上面省略的代码,

public abstract class ProxySelector {
    private static ProxySelector theProxySelector;
    static {
        try {
            Class<?> c = Class.forName("sun.net.spi.DefaultProxySelector");
            if (c != null && ProxySelector.class.isAssignableFrom(c)) {
                theProxySelector = (ProxySelector) c.newInstance();
            }
        } catch (Exception e) {
            theProxySelector = null;
        }
    }
    ...
}

而sun.net.spi.DefaultProxySelector这个实现会自己去读取系统的代理设置,这样就可以实现背景里提到的自动抓包了.

Dart 中Http的实现

上面Java可以自动去读取系统设置的代理,那么Dart的网络实现部分,难道没有类似的实现吗?不可以直接读取到系统设置的代理吗?查看Dart代码,也有类似的实现,如下

追看_findProxyFromEnvironment方法,进入到http_impl.dart中发现

static Map<String, String> _platformEnvironmentCache = Platform.environment;

但是就卡在这了,找不到Platform.environment的实现,那么Android/IOS 就读不到系统的代理设置,就不能像原生那样正常抓包了.

如果有读者找到这个对应的实现Platform.environment的网络代理部分,可以麻烦告诉我一下.

Flutter 之解决方法

为了不被产品经理,QA怼,为啥你这个就不行了呢?自己上吧,手动写一个代理设置器,让Flutter也可以正常抓包. 先看一下实现效果

实现

UI部分很简单,就不在写了,看一下网络部分怎么写的,我们网络直接使用了官方的Http,未直接使用DIO,因为那会DIO,还没出来呢.

第一步自己定义一个AppClinet,非关键部分

class AppClient extends http.BaseClient {
  // ignore: non_constant_identifier_names
  static final int TIME_OUT = 15;
  http.Client _innerClient;

  AppClient(this._innerClient);

  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) async {
    try {
      request.headers.addAll(getHeaders());
      Request httpRequest = request;
      AppUtils.log(httpRequest.headers.toString());
      //AppUtils.log(httpRequest.bodyFields.toString());
      return await _innerClient.send(request);
    } catch (e) {
      print('http error =' + e.toString());
      return Future.value(new http.StreamedResponse(new Stream.empty(), 400));
    }
  }
  ....
}

第二步,创建一个可以设置代理的HttpClient

  void lazyInitClient(bool isForce) {
    if (isForce || _appClient == null) {
      HttpClient httpClient = new HttpClient();
      if (AppConstants.DEVELOP) {
        //environment: {'http_proxy': '192.168.11.64:8888'}
        AppUtils.getProxy().then((str) {
          Map<String, String> map = {};
          print("str=" + str);
          httpClient.findProxy = (url) {
            String proxy = '';
            if (str.isNotEmpty) {
              map.addAll({'http_proxy': str, 'https_proxy': str});
              print(map.toString());
              proxy =
                  HttpClient.findProxyFromEnvironment(url, environment: map);
            } else {
              proxy = HttpClient.findProxyFromEnvironment(url);
            }
            print("proxy=" + proxy);
            return proxy;
          };
        });
      }
      http.Client client = new IOClient(httpClient);
      _appClient = new AppClient(client);
    }
  }

其中AppUtils为存储里面直接获取,避免每次重启都需要设置

//AppUtils
 static Future<String> getProxy() async {
    return await SharedPreferences.getInstance().then((prefs) {
      String proxy = prefs.getString("proxy");
      return proxy == null ? "" : proxy;
    });
  }

这样就完成了,Flutter中Dart的网络请求也可以抓包了。

其实那样是有缺点的,需要开发界面,手动设置。不需要开发界面,去自己实现一套Platform.environment Android/IOS 分别获取系统的代理,通过MethodChannel设置到网络请求中即可 。

总结

本文主要回顾了一下网络中代理的技术实现,并应用到跨平台中,这也是之前的一些踩坑实践分享出来,如果你觉得对你有帮助,欢迎点赞,谢谢.

推荐阅读作者的其他文章

Android:让你的“女神”逆袭,代码撸彩妆(画妆)
Flutter PIP(画中画)效果的实现
Android 绘制原理浅析【干货】