Flutter 跨端網路抓包 (以Android 為例)

北斗星_And發表於2019-10-09

背景

在很多公司測試環境使用的是內網測試,我們公司也是。
但是我們有點扯的是內網的域名沒有配置內網域名解析,必須手動配置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

Flutter 跨端網路抓包 (以Android 為例)
然後在手機上鍊接電腦的IP
Flutter 跨端網路抓包 (以Android 為例)

但是我們使用了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程式碼,也有類似的實現,如下

Flutter 跨端網路抓包 (以Android 為例)

追看_findProxyFromEnvironment方法,進入到http_impl.dart中發現

static Map<String, String> _platformEnvironmentCache = Platform.environment;
複製程式碼

但是就卡在這了,找不到Platform.environment的實現,那麼Android/IOS 就讀不到系統的代理設定,就不能像原生那樣正常抓包了.

如果有讀者找到這個對應的實現Platform.environment的網路代理部分,可以麻煩告訴我一下.

Flutter 之解決方法

為了不被產品經理,QA懟,為啥你這個就不行了呢?自己上吧,手動寫一個代理設定器,讓Flutter也可以正常抓包. 先看一下實現效果

Flutter 跨端網路抓包 (以Android 為例)

Flutter 跨端網路抓包 (以Android 為例)

實現

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 繪製原理淺析【乾貨】

相關文章