banner
Birdgg

Birdgg

Keep it simple, stupid!
bilibili
github
twitter
telegram
email

Document the solution to the WebView request proxy issue

image

Previously received product requirements needed to support user binding to Steam. Due to well-known reasons, we cannot directly access the Steam Community, so we need to proxy Steam requests within the webview.

iOS#

On iOS, Apple added the WKURLSchemeHandler protocol to WKWebView, which allows adding handling methods for custom schemes. We can proxy requests in the startURLSchemeTask method.

@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;

    // Set proxy
    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
{
  // Handle redirection
  // Requests intercepted by WKURLSchemeHandler do not support URL redirection, need to handle it manually
  completionHandler(nil);
}
@end

However, this protocol can only handle custom schemes, while schemes like https are already handled by default. Therefore, we need to bypass the restriction using a hook method.

@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

Finally, set the handler in WKWebviewConfiguration

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

Android#

Android adopts a more compatible way to proxy requests, calling setProxy when entering the webview and revertProxy when exiting.

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.