Proxy-Aware Apps
In the realm of modern software development and testing, understanding the concept of proxy-aware apps and their importance is crucial, especially when utilizing certain features offered by BrowserStack. If the HTTP client library used in the apps is able to detect and configure the system proxy, then those apps are called Proxy Aware. In this article, we will delve into the significance of proxy-aware apps and how they play a vital role in enabling the utilization of various BrowserStack features.
Example App
git clone https://github.com/BrowserStackCE/ProxyAwareAppExamples.git
HTTP clients libraries
Most mobile apps make HTTP/HTTPS-based API calls to various servers (internal or over the internet) to send and receive data. In order to make these API calls the apps to need to configure an HTTP Client. An HttpClient can be used to send requests and retrieve their responses. There are multiple client libraries used for both Android and iOS like HTTPUrlConnection, Dio, Volley, LoopJ, HTTPClientHandler, NSURLConnections, CFNetwork, etc.
Proxy Unaware Apps
Apps that make HTTP/HTTPS requests using HTTP client libraries that are not able to detect the proxy set up within the system automatically are called proxy unaware apps. This documentation will help you with making such HTTP Client proxy aware if they support an external method of specifying proxy that is to be used.
When is this required on BrowserStack?
When you are using features like
- Local Testing
- Inbound IP Whitelisting
- Network Logs
- IP Geolocation with BrowserStack
Why is this required on BrowserStack?
For BrowserStack to be able to provide you with features such are Local Testing / Inbound IP Whitelisting & Capturing NetworkLogs, BrowserStack is required to intercept your outgoing network requests. This is possible by setting up a proxy that can intercept these requests and forward them to the right device in our network.
-
Local Testing - In the case of Local Testing, network requests are forwarded to the machine that is running our local binary which is done with the help of proxy.
-
IP Whitelisting - In the case of IP Whitelisting, we set up a network machine with a selected IP. We share the IP with customers which they can whitelist at their end to allow incoming requests. Since this network machine is different from the device on which the tests are running the network requests need’s to be forwarded to the network machine and hence a proxy is used here.
-
Network Logs - As the name suggests this functionality provides you with network logs to all the network traffic done during your session, and to be able to capture those a proxy is required.
-
IP Geolocation - IP Geolocation provides you with functionality to mock traffic to your backend from an IP of a selected location. BrowserStack currently has Data Centers across 13 different locations but with our IP Geolocation feature, we provide you IP’s for 60+ countries. We achieve this by having a network device associated with the IP of the selected region make a network request to your backend. Hence capturing and forwarding those requests is done using a proxy.
Why should you make your app proxy aware?
Consider a situation where your user is under a corporate firewall with a proxy setup for security purposes. Will he be able to use your application? No. Hence your application not being proxy aware can be one of a corner case scenario where your user is behind a proxy and is not able to use your application.
Solutions
To make an application proxy aware the solution depends on which framework and HTTP Client you use.
Native Android (Android Studio)
Common Function to Fetch Proxy
private fun findProxy(uri: URI): Proxy? {
try {
val selector = ProxySelector.getDefault()
val proxyList = selector.select(uri)
// Look for HTTP Proxy
return proxyList.find { p -> p.type() == Proxy.Type.HTTP }
} catch (e: IllegalArgumentException) {
}
return Proxy.NO_PROXY
}
HTTPUrlConnection
- Passing Proxy to “openConnection” Method
fun HttpUrlConnectionRequest(url: URI){
Thread(Runnable {
var proxy = findProxy(url);
var conn:HttpURLConnection
if(proxy != null && proxy.type() == Proxy.Type.HTTP) {
conn = url.toURL().openConnection(proxy) as HttpURLConnection;
}else{
conn = url.toURL().openConnection() as HttpURLConnection;
}
conn.requestMethod = "GET"
val responseCode: Int = conn.getResponseCode()
if(responseCode == HttpURLConnection.HTTP_OK){
var inp = BufferedReader(InputStreamReader(conn.inputStream));
var response: String;
var responseBuffer = StringBuffer()
while(true){
response = inp.readLine() ?: break;
responseBuffer.append(response)
}
inp.close();
findViewById<TextView>(R.id.textView).setText(responseBuffer.toString());
}
}).start()
}
Volley
- Extending the HurlStack Class
class ProxiedHurlStack : HurlStack() {
private fun findProxy(uri: URI): Proxy? {
try {
val selector = ProxySelector.getDefault()
val proxyList = selector.select(uri)
if (proxyList.size > 1) return proxyList[0]
} catch (e: IllegalArgumentException) {
}
return Proxy.NO_PROXY
}
@Throws(IOException::class)
override fun createConnection(url: URL): HttpURLConnection {
val proxy: Proxy? = findProxy(url.toURI());
if (proxy != null && proxy.type() == Proxy.Type.HTTP) {
return url.openConnection(proxy) as HttpURLConnection
} else {
return url.openConnection() as HttpURLConnection
}
}
}
- Usage with Volley
fun VolleyRequest(url: URI){
val mRequestQueue = Volley.newRequestQueue(baseContext, ProxiedHurlStack())
val stringRequest = StringRequest(Request.Method.GET,url.toString(),Response.Listener<String> { response ->
// Do something with the response
findViewById<TextView>(R.id.textView).setText(response.toString());
},
Response.ErrorListener { error ->
// Handle error
})
mRequestQueue.add(stringRequest);
}
LoopJ
- Create new Async Client and Set Proxy
fun LoopJRequest(url: URI){
var proxy = findProxy(url);
var HttpClient = AsyncHttpClient()
if(proxy != null && proxy.type() == Proxy.Type.HTTP) {
val addr: InetSocketAddress = proxy.address() as InetSocketAddress
HttpClient.setProxy(addr.hostString,addr.port);
}
HttpClient.get(url.toString(), object : AsyncHttpResponseHandler() {
override fun onStart() {
// called before request is started
}
override fun onSuccess(
statusCode: Int,
headers: Array<Header?>?,
response: ByteArray?
) {
if(response != null) {
findViewById<TextView>(R.id.textView).setText(String(response, Charsets.UTF_8))
}
}
override fun onFailure(
statusCode: Int,
headers: Array<Header?>?,
errorResponse: ByteArray?,
e: Throwable?
) {
// called when response HTTP status is "4XX" (eg. 401, 403, 404)
}
override fun onRetry(retryNo: Int) {
// called when request is retried
}
})
}
Native IOS (Xcode)
- Fetching system proxy settings
let systemProxySettings = CFNetworkCopySystemProxySettings()?.takeUnretainedValue() ?? [:] as CFDictionary
-
CFNetworkCopySystemProxySettings() contains different key values pairs out of which the following 3 decides which type of proxy is present to be used
-
ProxyAutoConfigEnable
- If the value is 1 then there is a PAC File present and it needs to be executed to get the proxy details. -
HTTPSEnable
- If the value is 1 HTTPS proxy is available in the network. You check if the URL contains “https” and then fetch the host and port from the dictionary if not “https” use the HTTP proxy as mentioned below. -
HTTPEnable
- If the value is 1 HTTP proxy is available in the network. Fetch the host and port as below.
-
func findProxyFromEnvironment(url: String,callback: @escaping (_ host:String?,_ port:Int?)->Void) {
let proxConfigDict = CFNetworkCopySystemProxySettings()?.takeUnretainedValue() as NSDictionary?
if(proxConfigDict != nil){
if(proxConfigDict!["ProxyAutoConfigEnable"] as? Int == 1){
let pacUrl = proxConfigDict!["ProxyAutoConfigURLString"] as? String
let pacContent = proxConfigDict!["ProxyAutoConfigJavaScript"] as? String
if(pacContent != nil){
self.handlePacContent(pacContent: pacContent! as String, url: url, callback: callback)
}
downloadPac(pacUrl: pacUrl!, callback: { pacContent,error in
if(error != nil){
callback(nil,nil)
}else{
self.handlePacContent(pacContent: pacContent!, url: url, callback: callback)
}
})
} else if (proxConfigDict!["HTTPEnable"] as? Int == 1){
callback((proxConfigDict!["HTTPProxy"] as? String),(proxConfigDict!["HTTPPort"] as? Int))
} else if ( proxConfigDict!["HTTPSEnable"] as? Int == 1){
callback((proxConfigDict!["HTTPSProxy"] as? String),(proxConfigDict!["HTTPSPort"] as? Int))
} else {
callback(nil,nil)
}
}
}
func handlePacContent(pacContent: String,url: String, callback:(_ host:String?,_ port:Int?)->Void){
let proxies = CFNetworkCopyProxiesForAutoConfigurationScript(pacContent as CFString, CFURLCreateWithString(kCFAllocatorDefault, url as CFString, nil), nil)!.takeUnretainedValue() as? [[AnyHashable: Any]] ?? [];
if(proxies.count > 0){
let proxy = proxies[0]
if(proxy[kCFProxyTypeKey] as! CFString == kCFProxyTypeHTTP || proxy[kCFProxyTypeKey] as! CFString == kCFProxyTypeHTTPS){
let host = proxy[kCFProxyHostNameKey]
let port = proxy[kCFProxyPortNumberKey]
callback(host as? String,port as? Int)
}else{
callback(nil,nil)
}
}else{
callback(nil,nil)
}
}
func downloadPac(pacUrl:String, callback:@escaping (_ pacContent:String?,_ error: Error?)->Void) {
var pacContent:String = ""
let config = URLSessionConfiguration.default
config.connectionProxyDictionary = [AnyHashable: Any]()
let session = URLSession.init(configuration: config,delegate: nil,delegateQueue: OperationQueue.current)
session.dataTask(with: URL(string: pacUrl)!, completionHandler: { data, response, error in
if(error != nil){
callback(nil,error)
}
pacContent = String(bytes: data!,encoding: String.Encoding.utf8)!
callback(pacContent,nil)
}).resume()
}
URLConnection
let url = "http://ip-api.com/json"
findProxyFromEnvironment(url: url ,callback: { host,port in
let config = URLSessionConfiguration.default
if(host != nil && port != nil){
config.connectionProxyDictionary = [AnyHashable: Any]()
config.connectionProxyDictionary?[kCFNetworkProxiesHTTPEnable as String] = 1
config.connectionProxyDictionary?[kCFNetworkProxiesHTTPProxy as String] = host
config.connectionProxyDictionary?[kCFNetworkProxiesHTTPPort as String] = port
}
let session = URLSession(configuration: config)
let task = session.dataTask(with: URL(string: url)!)
{ data, response, error in
// Use data, response error
}
task.resume()
})
Flutter
Installation
dependencies:
...
flutter_system_proxy:
git:
url: https://github.com/BrowserStackCE/flutter_system_proxy.git
ref: main
Dio
// Create a custom adapter that can help resolve proxy based on urls
// (This is important as in some senerio there are PAC files which might have different proxy based on different urls)
class MyAdapter extends HttpClientAdapter {
final DefaultHttpClientAdapter _adapter = DefaultHttpClientAdapter();
@override
Future<ResponseBody> fetch(RequestOptions options,
Stream<Uint8List>? requestStream, Future? cancelFuture) async {
var uri = options.uri;
var proxy =
await FlutterSystemProxy.findProxyFromEnvironment(uri.toString()); // This line does the magic
_adapter.onHttpClientCreate = (HttpClient clinet) {
clinet.findProxy = (uri) {
return proxy;
};
};
return _adapter.fetch(options, requestStream, cancelFuture);
}
@override
void close({bool force = false}) {
_adapter.close(force: force);
}
}
// Use a wrapper around getting dio
void getDio(){
var dio = Dio();
dio.httpClientAdapter = MyAdapter();
return dio;
}
Need Help?
Please Contact support.
We're sorry to hear that. Please share your feedback so we can do better
Contact our Support team for immediate help while we work on improving our docs.
We're continuously improving our docs. We'd love to know what you liked
We're sorry to hear that. Please share your feedback so we can do better
Contact our Support team for immediate help while we work on improving our docs.
We're continuously improving our docs. We'd love to know what you liked
Thank you for your valuable feedback!