banner
Birdgg

Birdgg

Keep it simple, stupid!
bilibili
github
twitter
telegram
email

记录下解决 WebView 请求代理问题

image

之前接到产品的需求,需要支持用户绑定 steam。由于众所周知的原因,我们无法直接访问 steam 社区,因此需要在 webview 内对 steam 请求进行代理。

iOS

iOS 上,苹果为 WKWebView 添加了 WKURLSchemeHandler 协议,可以为自定义的 Scheme 添加处理方法。 我们可以在 startURLSchemeTask 方法中代理请求。

@implementation CustomURLSchemeHandler
-(instancetype)init {
  self.holdUrlSchemeTasks = [[NSMutableDictionary alloc] init];
  return self;
}

- (void)webView:(nonnull WKWebView *)webView startURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
  _holdUrlSchemeTasks[urlSchemeTask.description] = @YES;
  NSURLRequest *request = urlSchemeTask.request;
  NSURLSessionConfiguration *configuration = NSURLSessionConfiguration.defaultSessionConfiguration;

    // 设置代理
    NSDictionary *connectionProxyDictionary = @{
      (NSString *)kCFNetworkProxiesHTTPEnable: [NSNumber numberWithInt: 1],
      (NSString *)kCFNetworkProxiesHTTPProxy: self.server,
      (NSString *)kCFNetworkProxiesHTTPPort: self.port,
      @"HTTPSEnable": [NSNumber numberWithInt: 1],
      @"HTTPSProxy": self.server,
      @"HTTPSPort": self.port,
    };
    [configuration setConnectionProxyDictionary:connectionProxyDictionary];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];

    NSURLSessionTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
      if(![self->_holdUrlSchemeTasks[urlSchemeTask.description] boolValue]) {
        return;
      }
      if(error) {
        [urlSchemeTask didFailWithError:error];
      } else {
        [urlSchemeTask didReceiveResponse:response];
        [urlSchemeTask didReceiveData:data];
        [urlSchemeTask didFinish];
      }
    }];
  

  [task resume];
}

- (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
  _holdUrlSchemeTasks[urlSchemeTask.description] = @NO;
}

- (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
        newRequest:(NSURLRequest *)request
 completionHandler:(void (^)(NSURLRequest *))completionHandler
{
  // 处理重定向跳转
  // WKURLSchemeHandler 拦截的请求不支持 url 重定向跳转,需要自己支持
  completionHandler(nil);
}
@end

但是该协议只能处理自定义 Scheme,而 https 这种 Scheme 已经默认处理了。所以需要通过 Hook 的方法绕过限制。

@implementation WKWebView (SchemeHandle)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSelector = @selector(handlesURLScheme:);
        SEL swizzledSelector = @selector(cdz_handlesURLScheme:);
        Method originalMethod = class_getClassMethod(self, originalSelector);
        Method swizzledMethod = class_getClassMethod(self, swizzledSelector);
        method_exchangeImplementations(originalMethod, swizzledMethod);

    });
}

+ (BOOL)cdz_handlesURLScheme:(NSString *)urlScheme {
    if ([urlScheme isEqualToString:@"http"] || [urlScheme isEqualToString:@"https"]) {
        return NO;
    }
    return [self handlesURLScheme:urlScheme];
}

@end

最后在 WKWebviewConfiguration 中设置 handler

WKWebViewConfiguration *wkWebViewConfig = [WKWebViewConfiguration new];
CustomURLSchemeHandler *handler = [CustomURLSchemeHandler new];
[wkWebViewConfig setURLSchemeHandler:handler forURLScheme:@"https"];
[wkWebViewConfig setURLSchemeHandler:handler forURLScheme:@"http"];

Android

Android 采用以下兼容性更好的方式来进行请求代理,在进入 webview 的时候调用 setProxy, 退出的时候调用 revertProxy

public class ProxySetting {
  private static final String TAG = "ProxySetting";
  private static final String APPLICATION_NAME = "android.app.Application";

  public static boolean setProxy(WebView webView, String str, int i) {
    if (Build.VERSION.SDK_INT <= 15) {
      return setProxyICS(webView, str, i);
    } else if (Build.VERSION.SDK_INT <= 18) {
      return setProxyJB(webView, str, i);
    } else {
      return setProxyKKPlus(webView, str, i);
    }
  }

  public static boolean revertProxy(WebView webview) {
    if (Build.VERSION.SDK_INT <= 15) {
      return revertProxyICS(webview);
    } else if (Build.VERSION.SDK_INT <= 18) {
      return revertProxyJB(webview);
    } else {
      return revertProxyKKPlus(webview);
    }
  }

  private static boolean setProxyICS(WebView webView, String str, int i) {
    try {
      Class.forName("android.webkit.JWebCoreJavaBridge").getDeclaredMethod("updateProxy", Class.forName("android.net.ProxyProperties")).invoke(getFieldValueSafely(Class.forName("android.webkit.BrowserFrame").getDeclaredField("sJavaBridge"), getFieldValueSafely(Class.forName("android.webkit.WebViewCore").getDeclaredField("mBrowserFrame"), getFieldValueSafely(Class.forName("android.webkit.WebView").getDeclaredField("mWebViewCore"), webView))), Class.forName("android.net.ProxyProperties").getConstructor(String.class, Integer.TYPE, String.class).newInstance(str, Integer.valueOf(i), null));
      return true;
    } catch (Exception unused) {
      return false;
    }
  }

  private static boolean revertProxyICS(WebView webview) {
    try {
      Log.d(TAG, "Setting proxy with 4.0 API.");
      Class jwcjb = Class.forName("android.webkit.JWebCoreJavaBridge");
      Class params[] = new Class[1];
      params[0] = Class.forName("android.net.ProxyProperties");
      Method updateProxyInstance = jwcjb.getDeclaredMethod("updateProxy", params);

      Class wv = Class.forName("android.webkit.WebView");
      Field mWebViewCoreField = wv.getDeclaredField("mWebViewCore");
      Object mWebViewCoreFieldInstance = getFieldValueSafely(mWebViewCoreField, webview);

      Class wvc = Class.forName("android.webkit.WebViewCore");
      Field mBrowserFrameField = wvc.getDeclaredField("mBrowserFrame");
      Object mBrowserFrame = getFieldValueSafely(mBrowserFrameField, mWebViewCoreFieldInstance);

      Class bf = Class.forName("android.webkit.BrowserFrame");
      Field sJavaBridgeField = bf.getDeclaredField("sJavaBridge");
      Object sJavaBridge = getFieldValueSafely(sJavaBridgeField, mBrowserFrame);

      Class ppclass = Class.forName("android.net.ProxyProperties");
      Class pparams[] = new Class[3];
      pparams[0] = String.class;
      pparams[1] = int.class;
      pparams[2] = String.class;
      Constructor ppcont = ppclass.getConstructor(pparams);

      Object o = null;
      updateProxyInstance.invoke(sJavaBridge, o);

      Log.d(TAG, "Setting proxy with 4.0 API successful!");
      return true;
    } catch (Exception ex) {
      Log.e(TAG, "failed to set HTTP proxy: " + ex);
      return false;
    }
  }

  private static boolean setProxyJB(WebView webView, String str, int i) {
    try {
      Object fieldValueSafely = getFieldValueSafely(Class.forName("android.webkit.BrowserFrame").getDeclaredField("sJavaBridge"), getFieldValueSafely(Class.forName("android.webkit.WebViewCore").getDeclaredField("mBrowserFrame"), getFieldValueSafely(Class.forName("android.webkit.WebViewClassic").getDeclaredField("mWebViewCore"), Class.forName("android.webkit.WebViewClassic").getDeclaredMethod("fromWebView", Class.forName("android.webkit.WebView")).invoke(null, webView))));
      Constructor<?> constructor = Class.forName("android.net.ProxyProperties").getConstructor(String.class, Integer.TYPE, String.class);
      Class.forName("android.webkit.JWebCoreJavaBridge").getDeclaredMethod("updateProxy", Class.forName("android.net.ProxyProperties")).invoke(fieldValueSafely, constructor.newInstance(str, Integer.valueOf(i), null));
      return true;
    } catch (Exception unused) {
      return false;
    }
  }

  private static boolean revertProxyJB(WebView webview) {
    Log.d(TAG, "revert proxy with 4.1 - 4.3 API.");
    try {
      Class wvcClass = Class.forName("android.webkit.WebViewClassic");
      Class wvParams[] = new Class[1];
      wvParams[0] = Class.forName("android.webkit.WebView");
      Method fromWebView = wvcClass.getDeclaredMethod("fromWebView", wvParams);
      Object webViewClassic = fromWebView.invoke(null, webview);

      Class wv = Class.forName("android.webkit.WebViewClassic");
      Field mWebViewCoreField = wv.getDeclaredField("mWebViewCore");
      Object mWebViewCoreFieldInstance = getFieldValueSafely(mWebViewCoreField, webViewClassic);

      Class wvc = Class.forName("android.webkit.WebViewCore");
      Field mBrowserFrameField = wvc.getDeclaredField("mBrowserFrame");
      Object mBrowserFrame = getFieldValueSafely(mBrowserFrameField, mWebViewCoreFieldInstance);

      Class bf = Class.forName("android.webkit.BrowserFrame");
      Field sJavaBridgeField = bf.getDeclaredField("sJavaBridge");
      Object sJavaBridge = getFieldValueSafely(sJavaBridgeField, mBrowserFrame);

      Class ppclass = Class.forName("android.net.ProxyProperties");
      Class pparams[] = new Class[3];
      pparams[0] = String.class;
      pparams[1] = int.class;
      pparams[2] = String.class;
      Constructor ppcont = ppclass.getConstructor(pparams);

      Class jwcjb = Class.forName("android.webkit.JWebCoreJavaBridge");
      Class params[] = new Class[1];
      params[0] = Class.forName("android.net.ProxyProperties");
      Method updateProxyInstance = jwcjb.getDeclaredMethod("updateProxy", params);

      Object o = null;
      updateProxyInstance.invoke(sJavaBridge, o);
    } catch (Exception ex) {
      Log.e(TAG, "Setting proxy with >= 4.1 API failed with error: " + ex.getMessage());
      return false;
    }

    Log.d(TAG, "revert proxy with 4.1 - 4.3 API successful!");
    return true;
  }

  private static boolean setProxyKKPlus(WebView webView, String host, int port) {
    Context appContext = webView.getContext().getApplicationContext();
    System.setProperty("http.proxyHost", host);
    System.setProperty("http.proxyPort", port + "");
    System.setProperty("https.proxyHost", host);
    System.setProperty("https.proxyPort", port + "");
    try {
      Field loadedApkField = Class.forName(APPLICATION_NAME).getField("mLoadedApk");
      loadedApkField.setAccessible(true);
      Object loadedApk = loadedApkField.get(appContext);

      Field receiversField = Class.forName("android.app.LoadedApk").getDeclaredField("mReceivers");
      receiversField.setAccessible(true);
      ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk);
      for (Object receiverMap : receivers.values()) {
        for (Object rec : ((ArrayMap) receiverMap).keySet()) {
          Class clazz = rec.getClass();
          if (clazz != null && clazz.getName().contains("ProxyChangeListener")) {
            Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class, Intent.class);
            Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
            onReceiveMethod.invoke(rec, appContext, intent);
          }
        }
      }
      return true;
    } catch (ClassNotFoundException e) {
      StringWriter stringWriter = new StringWriter();
      e.printStackTrace(new PrintWriter(stringWriter));
      stringWriter.toString();
      return false;
    } catch (NoSuchFieldException e2) {
      StringWriter stringWriter2 = new StringWriter();
      e2.printStackTrace(new PrintWriter(stringWriter2));
      stringWriter2.toString();
      return false;
    } catch (IllegalAccessException e3) {
      StringWriter stringWriter3 = new StringWriter();
      e3.printStackTrace(new PrintWriter(stringWriter3));
      stringWriter3.toString();
      return false;
    } catch (IllegalArgumentException e4) {
      StringWriter stringWriter4 = new StringWriter();
      e4.printStackTrace(new PrintWriter(stringWriter4));
      stringWriter4.toString();
      return false;
    } catch (NoSuchMethodException e5) {
      StringWriter stringWriter5 = new StringWriter();
      e5.printStackTrace(new PrintWriter(stringWriter5));
      stringWriter5.toString();
      return false;
    } catch (InvocationTargetException e6) {
      StringWriter stringWriter6 = new StringWriter();
      e6.printStackTrace(new PrintWriter(stringWriter6));
      stringWriter6.toString();
      return false;
    }
  }

  public static boolean revertProxyKKPlus(WebView webView) {
    Context appContext = webView.getContext().getApplicationContext();
    Properties properties = System.getProperties();
    properties.remove("http.proxyHost");
    properties.remove("http.proxyPort");
    properties.remove("https.proxyHost");
    properties.remove("https.proxyPort");
    try {
      Field loadedApkField = Class.forName(APPLICATION_NAME).getField("mLoadedApk");
      loadedApkField.setAccessible(true);
      Object loadedApk = loadedApkField.get(appContext);
      Field receiversField = Class.forName("android.app.LoadedApk").getDeclaredField("mReceivers");
      receiversField.setAccessible(true);
      ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk);
      for (Object receiverMap : receivers.values()) {
        for (Object rec : ((ArrayMap) receiverMap).keySet()) {
          Class clazz = rec.getClass();
          if (clazz != null && clazz.getName().contains("ProxyChangeListener")) {
            Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class, Intent.class);
            Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
            onReceiveMethod.invoke(rec, appContext, intent);
          }
        }
      }
      return true;
    } catch (ClassNotFoundException e) {
      StringWriter stringWriter = new StringWriter();
      e.printStackTrace(new PrintWriter(stringWriter));
      stringWriter.toString();
      return false;
    } catch (NoSuchFieldException e2) {
      StringWriter stringWriter2 = new StringWriter();
      e2.printStackTrace(new PrintWriter(stringWriter2));
      stringWriter2.toString();
      return false;
    } catch (IllegalAccessException e3) {
      StringWriter stringWriter3 = new StringWriter();
      e3.printStackTrace(new PrintWriter(stringWriter3));
      stringWriter3.toString();
      return false;
    } catch (IllegalArgumentException e4) {
      StringWriter stringWriter4 = new StringWriter();
      e4.printStackTrace(new PrintWriter(stringWriter4));
      stringWriter4.toString();
      return false;
    } catch (NoSuchMethodException e5) {
      StringWriter stringWriter5 = new StringWriter();
      e5.printStackTrace(new PrintWriter(stringWriter5));
      stringWriter5.toString();
      return false;
    } catch (InvocationTargetException e6) {
      StringWriter stringWriter6 = new StringWriter();
      e6.printStackTrace(new PrintWriter(stringWriter6));
      stringWriter6.toString();
      return false;
    }
  }

  private static Object getFieldValueSafely(Field field, Object obj) throws IllegalArgumentException, IllegalAccessException {
    boolean isAccessible = field.isAccessible();
    field.setAccessible(true);
    Object obj2 = field.get(obj);
    field.setAccessible(isAccessible);
    return obj2;
  }
}

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.