Inspect Full http and HTTPS requests made by webviews in android

 Hi guys welcome to my Blog post. In this post Iam going to show you all that how can we Inpsect http or https requests including all headers, cookies and body that are sent by webviews in our android app. This full source code will be found in my github repository 


Implementation 

You can download source code from my GitHub repository and copy RequestInspect package to your project or copy the code from this post at last.
Add webview to your xml activity class.
<WebView 
android:id="@+id/WebView"
android:layout_height="match_parent"
android:layout_width="match_parent"/>
Initialize and load url into webview in your java activity class.
WebView webview=findViewById(R.id.WebView);
webview.loadUrl("https://www.hackerone.com/");

set WebViewClient to webview

webview.setWebViewClient(new RequestInspectorWebViewClient(webview))

That's it now create the app by clicking on run.

Copy the code from below if you didn't download from github.


RecordedRequest.java

import java.util.Map;
public final class RecordedRequest {
	WebViewRequestType type;
	String method;
	String url;
	String body;
	String trace;
	Map<String,String>headers;
	String encType;

	public RecordedRequest(WebViewRequestType type, String method, String url, String body, String trace, Map<String,String>headers, String encType) {
		this.type = type;
		this.method = method;
		this.url = url;
		this.body = body;
		this.trace = trace;
		this.headers = headers;
		this.encType = encType;
	}
	
	public void setType(WebViewRequestType type) {
		this.type = type;
	}
	public WebViewRequestType getType() {
		return type;
	}
	public void setMethod(String method) {
		this.method = method;
	}

	public String getMethod() {
		return method;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	public String getUrl() {
		return url;
	}

	public void setBody(String body) {
		this.body = body;
	}

	public String getBody() {
		return body;
	}

	public void setTrace(String trace) {
		this.trace = trace;
	}

	public String getTrace() {
		return trace;
	}

	public void setHeaders(Map<String, String> headers) {
		this.headers = headers;
	}

	public Map<String, String> getHeaders() {
		return headers;
	}

	public void setEncType(String encType) {
		this.encType = encType;
	}

	public String getEncType() {
		return encType;
	}}

WebViewRequest.java

import java.util.Map;
import java.util.stream.Collectors;
import java.util.Locale;
import android.webkit.WebResourceRequest;
import java.util.HashMap;
import android.webkit.CookieManager;
import android.os.Build;

public class WebViewRequest {
    private final WebViewRequestType type;
    private final String url;
    private final String method;
    private final String body;
    private final Map<String, String> headers;
    private final String trace;
    private final String enctype;
    private final boolean isForMainFrame;
    private final boolean isRedirect;
    private final boolean hasGesture;

    public WebViewRequest(
        WebViewRequestType type,
        String url,
        String method,
        String body,
        Map<String, String> headers,
        String trace,
        String enctype,
        boolean isForMainFrame,
        boolean isRedirect,
        boolean hasGesture
    ) {
        this.type = type;
        this.url = url;
        this.method = method;
        this.body = body;
        this.headers = headers;
        this.trace = trace;
        this.enctype = enctype;
        this.isForMainFrame = isForMainFrame;
        this.isRedirect = isRedirect;
        this.hasGesture = hasGesture;
    }

    public WebViewRequestType getType() {
        return type;
    }

    public String getUrl() {
        return url;
    }

    public String getMethod() {
        return method;
    }

    public String getBody() {
        return body;
    }

    public Map<String, String> getHeaders() {
        return headers;
    }

    public String getTrace() {
        return trace;
    }

    public String getEnctype() {
        return enctype;
    }

    public boolean isForMainFrame() {
        return isForMainFrame;
    }

    public boolean isRedirect() {
        return isRedirect;
    }

    public boolean hasGesture() {
        return hasGesture;
    }

    @Override
    public String toString() {
        StringBuilder traceStringBuilder = new StringBuilder();
		for (String traceLine : trace.split("\n")) {
			traceStringBuilder.append("    ").append(traceLine.trim()).append("\n");
		}
		String traceWithIndent = traceStringBuilder.toString();
		return String.format(Locale.US,
							 "Type: %s\n" +
							 "URL: %s\n" +
							 "Method: %s\n" +
							 "Body: %s\n" +
							 "Headers: %s" +
							 "Trace: %s" +
							 "Encoding type (form submissions only): %s\n" +
							 "Is for main frame? %s\n" +
							 "Is redirect? %s\n" +
							 "Has gesture? %s\n",
							 type,
							 url,
							 method,
							 body,
							 headers,
							 traceWithIndent,
							 enctype != null ? enctype : "",
							 isForMainFrame,
							 isRedirect,
							 hasGesture);
		
    }
	public static WebViewRequest create(WebResourceRequest webResourceRequest,RecordedRequest recordedRequest) {
    WebViewRequestType type = recordedRequest != null ? recordedRequest.getType() : WebViewRequestType.HTML;
    String url = webResourceRequest.getUrl().toString();
    String cookies = CookieManager.getInstance().getCookie(url) != null ? CookieManager.getInstance().getCookie(url) : "";
    HashMap<String, String> headers = new HashMap<>();
    headers.put("cookie", cookies);
    if (recordedRequest != null) {
        Map<String, String> recordedHeadersInLowercase = new HashMap<>();
        for (Map.Entry<String, String> entry : recordedRequest.getHeaders().entrySet()) {
            recordedHeadersInLowercase.put(entry.getKey().toLowerCase(), entry.getValue());
        }
        headers.putAll(recordedHeadersInLowercase);
    }
    Map<String, String> requestHeaders = new HashMap<>();
    for (Map.Entry<String, String> entry : webResourceRequest.getRequestHeaders().entrySet()) {
        requestHeaders.put(entry.getKey().toLowerCase(), entry.getValue());
    }
    headers.putAll(requestHeaders);
    boolean isRedirect = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? webResourceRequest.isRedirect() : false;
    return new WebViewRequest(
            type,
            url,
            webResourceRequest.getMethod(),
            recordedRequest != null ? recordedRequest.getBody() : "",
            headers,
            recordedRequest != null ? recordedRequest.getTrace() : "",
            recordedRequest != null ? recordedRequest.getEncType() : null,
            webResourceRequest.isForMainFrame(),
            isRedirect,
            webResourceRequest.hasGesture()
    );
}

}

WebViewRequestType.java

public enum WebViewRequestType {

    FETCH,
    XML_HTTP,
    FORM,
    HTML
   
    
    
}

RequestInspectorJavaScriptInterface.java

import android.webkit.WebView;
import java.util.ArrayList;
import java.util.Map;
import org.json.JSONArray;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.URLEncoder;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import android.webkit.JavascriptInterface;

public class RequestInspectorJavaScriptInterface {
    private WebView mWebView;
    public RequestInspectorJavaScriptInterface(WebView webView) {
		mWebView = webView;
		mWebView.addJavascriptInterface(this,RequestInspectorJs.INTERFACE_NAME);
        
    }
	private ArrayList<RecordedRequest> recordedRequests = new ArrayList<>();
	public RecordedRequest findRecordedRequestForUrl(String url) {
		synchronized (recordedRequests) {
			for (RecordedRequest recordedRequest : recordedRequests) {
				if (url.contains(recordedRequest.getUrl())) {
					return recordedRequest;
				}
			}
		}
		return null;
	}
	public void recordFormSubmission(String url, String method, String formParameterList, String headers, String trace, String enctype) throws JSONException, UnsupportedEncodingException {
	JSONArray formParameterJsonArray = new JSONArray(formParameterList);
		Map<String, String> headerMap = getHeadersAsMap(headers);
		String body = "";
		switch (enctype) {
			case "application/x-www-form-urlencoded":
				headerMap.put("content-type", enctype);
				body = getUrlEncodedFormBody(formParameterJsonArray);
				break;
			case "multipart/form-data":
				
				headerMap.put("content-type", "multipart/form-data; boundary=" + RequestInspectorJs.MULTIPART_FORM_BOUNDARY);
				body = getMultiPartFormBody(formParameterJsonArray);
				break;
			case "text/plain":
				headerMap.put("content-type", enctype);
				body = getPlainTextFormBody(formParameterJsonArray);
				break;
			default:
				Log.e(RequestInspectorJs.LOG_TAG, "Incorrect encoding received from JavaScript: " + enctype);
				break;
		}

		Log.i(RequestInspectorJs.LOG_TAG, "Recorded form submission from JavaScript");
		addRecordedRequest(new RecordedRequest(WebViewRequestType.FORM,method,url,body,trace,headerMap,enctype));
	}

	private void addRecordedRequest(RecordedRequest recordedRequest) {
		synchronized(recordedRequests) {
			recordedRequests.add(recordedRequest);
		}
	}
	

	private String getPlainTextFormBody(JSONArray formParameterJsonArray) throws JSONException {
		StringBuilder resultStringBuilder = new StringBuilder();
		for (int i = 0; i < formParameterJsonArray.length(); i++) {
			JSONObject formParameter = formParameterJsonArray.getJSONObject(i);
			String name = formParameter.getString("name");
			String value = formParameter.getString("value");
			if (i != 0) {
				resultStringBuilder.append("\n");
			}
			resultStringBuilder.append(name);
			resultStringBuilder.append("=");
			resultStringBuilder.append(value);
		}
		return resultStringBuilder.toString();
	}
	

	private String getMultiPartFormBody(JSONArray formParameterJsonArray) throws JSONException {
		StringBuilder resultStringBuilder = new StringBuilder();
		for (int i = 0; i < formParameterJsonArray.length(); i++) {
			JSONObject formParameter = formParameterJsonArray.getJSONObject(i);
			String name = formParameter.getString("name");
			String value = formParameter.getString("value");
			resultStringBuilder.append("--");
			resultStringBuilder.append(RequestInspectorJs.MULTIPART_FORM_BOUNDARY);
			resultStringBuilder.append("\n");
			resultStringBuilder.append("Content-Disposition: form-data; name=\"");
			resultStringBuilder.append(name);
			resultStringBuilder.append("\"\n\n");
			resultStringBuilder.append(value);
			resultStringBuilder.append("\n");
		}
		resultStringBuilder.append("--");
		resultStringBuilder.append(RequestInspectorJs.MULTIPART_FORM_BOUNDARY);
		resultStringBuilder.append("--");
		return resultStringBuilder.toString();
	}
	
	private String getUrlEncodedFormBody(JSONArray formParameterJsonArray) throws UnsupportedEncodingException, JSONException, UnsupportedEncodingException {
		StringBuilder resultStringBuilder = new StringBuilder();
		for (int i = 0; i < formParameterJsonArray.length(); i++) {
			JSONObject formParameter = formParameterJsonArray.getJSONObject(i);
			String name = formParameter.getString("name");
			String value = formParameter.getString("value");
			String encodedValue = URLEncoder.encode(value, "UTF-8");
			if (i != 0) {
				resultStringBuilder.append("&");
			}
			resultStringBuilder.append(name);
			resultStringBuilder.append("=");
			resultStringBuilder.append(encodedValue);
		}
		return resultStringBuilder.toString();
	}
	

	private Map<String, String> getHeadersAsMap(String headersString) throws JSONException {
		JSONObject headersObject = new JSONObject(headersString);
		Map<String, String> map = new HashMap<String, String>();
		Iterator<String> iterator = headersObject.keys();
		while (iterator.hasNext()) {
			String key = iterator.next();
			String lowercaseHeader = key.toLowerCase(Locale.getDefault());
			map.put(lowercaseHeader, headersObject.getString(key));
		}
		return map;
	}
	
	
	@JavascriptInterface
	public void recordXhr(String url, String method, String body, String headers, String trace) throws JSONException {
		Log.i(RequestInspectorJs.LOG_TAG, "Recorded XHR from JavaScript");
		Map<String, String> headerMap = getHeadersAsMap(headers);
		addRecordedRequest(
			new RecordedRequest(WebViewRequestType.FORM,method,url,body,trace,headerMap,null)
		);
	}

	@JavascriptInterface
	public void recordFetch(String url, String method, String body, String headers, String trace) throws JSONException {
		Log.i(RequestInspectorJs.LOG_TAG, "Recorded fetch from JavaScript");
		Map<String, String> headerMap = getHeadersAsMap(headers);
		addRecordedRequest(
			new RecordedRequest(WebViewRequestType.FORM,method,url,body,trace,headerMap,null)
		);
	}
	public class RequestInspectorJs {
		public static final String LOG_TAG = "RequestInspectorJs";
		public static final String MULTIPART_FORM_BOUNDARY = "----WebKitFormBoundaryU7CgQs9WnqlZYKs6";
		public static final String INTERFACE_NAME = "RequestInspection";
		public static final String JAVASCRIPT_INTERCEPTION_CODE="function getFullUrl(url) {\n"+
		"if (url.startsWith(\"/\")) {\n"+
		"return location.protocol + '//' + location.host + url;\n"+
		"} else {\n"+
		"return url;\n"+
		"}\n"+
		"}\n"+
		"function recordFormSubmission(form) {\n"+
		"var jsonArr = [];\n"+
		"for (i = 0; i < form.elements.length; i++) {\n"+
		"var parName = form.elements[i].name;\n"+
		"var parValue = form.elements[i].value;\n"+
		"var parType = form.elements[i].type;\n"+
		"jsonArr.push({\n"+
		"name: parName,\n"+
		"value: parValue,\n"+
		"type: parType\n"+
		"});\n"+
		"}\n"+
		"const path = form.attributes['action'] === undefined ? \"/\" : form.attributes['action'].nodeValue;\n"+
		"const method = form.attributes['method'] === undefined ? \"GET\" : form.attributes['method'].nodeValue;\n"+
		"const url = getFullUrl(path);\n"+
		"const encType = form.attributes['enctype'] === undefined ? \"application/x-www-form-urlencoded\" : form.attributes['enctype'].nodeValue;\n"+
		"const err = new Error();\n"+
		"$INTERFACE_NAME.recordFormSubmission(\n"+
		"url,\n"+
		"method,\n"+
		"JSON.stringify(jsonArr),\n"+
		"\"{}\",\n"+
		"err.stack,\n"+
		"encType\n"+
		");\n"+
		"}\n"+
		"function handleFormSubmission(e) {\n"+
		"const form = e ? e.target : this;\n"+
		"recordFormSubmission(form);\n"+
		"form._submit();\n"+
		"}\n"+
		"HTMLFormElement.prototype._submit = HTMLFormElement.prototype.submit;\n"+
		"HTMLFormElement.prototype.submit = handleFormSubmission;\n"+
		"window.addEventListener('submit', function (submitEvent) {\n"+
		"handleFormSubmission(submitEvent);\n"+
		"}, true);\n"+
		"let lastXmlhttpRequestPrototypeMethod = null;\n"+
		"let xmlhttpRequestHeaders = {};\n"+
		"let xmlhttpRequestUrl = null;\n"+
		"XMLHttpRequest.prototype._open = XMLHttpRequest.prototype.open;\n"+
		"XMLHttpRequest.prototype.open = function (method, url, async, user, password) {\n"+
		"lastXmlhttpRequestPrototypeMethod = method;\n"+
		"xmlhttpRequestUrl = url;\n"+
		"const asyncWithDefault = async === undefined ? true : async;\n"+
		"this._open(method, url, asyncWithDefault, user, password);\n"+
		"};\n"+
		"XMLHttpRequest.prototype._setRequestHeader = XMLHttpRequest.prototype.setRequestHeader;\n"+
		"XMLHttpRequest.prototype.setRequestHeader = function (header, value) {\n"+
		"xmlhttpRequestHeaders[header] = value;\n"+
		"this._setRequestHeader(header, value);\n"+
		"};\n"+
		"XMLHttpRequest.prototype._send = XMLHttpRequest.prototype.send;\n"+
		"XMLHttpRequest.prototype.send = function (body) {\n"+
		"const err = new Error();\n"+
		"const url = getFullUrl(xmlhttpRequestUrl);\n"+
		"$INTERFACE_NAME.recordXhr(\n"+
		"url,\n"+
		"lastXmlhttpRequestPrototypeMethod,\n"+
		"body || \"\",\n"+
		"JSON.stringify(xmlhttpRequestHeaders),\n"+
		"err.stack\n"+
		");\n"+
		"lastXmlhttpRequestPrototypeMethod = null;\n"+
		"xmlhttpRequestUrl = null;\n"+
		"xmlhttpRequestHeaders = {};\n"+
		"this._send(body);\n"+
		"};\n"+
		"window._fetch = window.fetch;\n"+
		"window.fetch = function () {\n"+
		"const url = arguments[1] && 'url' in arguments[1] ? arguments[1]['url'] : \"/\";\n"+
		"const fullUrl = getFullUrl(url);\n"+
		"const method = arguments[1] && 'method' in arguments[1] ? arguments[1]['method'] : \"GET\";\n"+
		"const body = arguments[1] && 'body' in arguments[1] ? arguments[1]['body'] : \"\";\n"+
		"const headers = JSON.stringify(arguments[1] && 'headers' in arguments[1] ? arguments[1]['headers'] : {});\n"+
		"let err = new Error();\n"+
		"$INTERFACE_NAME.recordFetch(fullUrl, method, body, headers, err.stack);\n"+
		"return window._fetch.apply(this, arguments);\n"+
		"}\n";
		
	}
	public static void enabledRequestInspection(WebView webView ) {
		String jsCode = RequestInspectorJs.JAVASCRIPT_INTERCEPTION_CODE + "\n";//extraJavaScriptToInject;
		webView.evaluateJavascript("javascript: " + jsCode, null);
	}
	

    // Methods to inspect network requests using JavaScript
}

RequestInspectOptions.java

public class RequestInspectOptions extends WebViewClient{
    String extraJavaScriptToInject="";
    
    
}


RequestInspectorWebViewClient.java

import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.util.Log;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class RequestInspectorWebViewClient extends WebViewClient {

    private static final String LOG_TAG = "RequestInspectorWebView";
    private RequestInspectorJavaScriptInterface interceptionJavascriptInterface;
    private RequestInspectOptions options;

    public RequestInspectorWebViewClient(WebView webView) {
        this(webView, new RequestInspectOptions());
    }
    public RequestInspectorWebViewClient(WebView webView, RequestInspectOptions options) {
        this.options = options;
        interceptionJavascriptInterface = new RequestInspectorJavaScriptInterface(webView);
    }

    @SuppressLint("SetJavaScriptEnabled")
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        Log.i(LOG_TAG, "Page started loading, enabling request inspection. URL: " + url);
        RequestInspectorJavaScriptInterface.enabledRequestInspection(view);
        super.onPageStarted(view, url, favicon);
    }

    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
		
        RecordedRequest recordedRequest = interceptionJavascriptInterface.findRecordedRequestForUrl(request.getUrl().toString());
        WebViewRequest webViewRequest = WebViewRequest.create(request, recordedRequest);
        return shouldInterceptRequest(view, webViewRequest);
    }

    public WebResourceResponse shouldInterceptRequest(WebView view, WebViewRequest webViewRequest) {
		
        logWebViewRequest(webViewRequest);
        return null;
    }
    protected void logWebViewRequest(WebViewRequest webViewRequest) {
        Log.i(LOG_TAG, "Sending request from WebView: " + webViewRequest.toString());
    }
}


Post a Comment (0)
Previous Post Next Post