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