ledy
ledy

Reputation: 1617

Android Webview: disable CORS

Does Android allow native apps to disable CORS security policies for http:// (not local/file) requests?

In my native app, a webview shows a remote html via http://, not on the local/file system. This seems to be CORS-restricted in the same way as within webbrowsers.

Worakround: A native-js bridge for ajax requests to cross-domains which do not have Access-Control-Allow-Origin: * is my quick'n'dirt solution. (jsonp or server-side proxy is not an option because cookie+ip of client are checked by the webservice.)

Can this policy be disabled for inapp webviews?

Please let me know, if there is a simple flag for allowing js to bypass this restriction which limits the "native" app's webview.

Upvotes: 29

Views: 55233

Answers (4)

hugo der hungrige
hugo der hungrige

Reputation: 12912

I am halfway there. Only thing missing is POST and PUT requests, that need more than just working around the options request:

fun interceptWebRequest(request: WebResourceRequest?): WebResourceResponse? {
    try {
        if (request == null || request.isForMainFrame) {
            return null
        }

        if (request.url?.path?.contains("assets/icons/favicon") == true) {
            try {
                return WebResourceResponse("image/png", null, null)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }

        if (request.url.toString().contains(serviceHost)) {
            return null
        }

        Log.v(
            "TW",
            "interceptRequest mf:${request?.isForMainFrame.toString()} ${request.method} ${request?.url}"
        )

        // since we currently don't have a way to also post the body, we only handle GET, HEAD and OPTIONS requests
        // see https://github.com/KonstantinSchubert/request_data_webviewclient for a possible solution
        if (request.method.uppercase() != "GET" && request.method.uppercase() != "OPTIONS" && request.method.uppercase() != "HEAD") {
            return null
        }

        val client = OkHttpClient()
        val newRequestBuilder = Request.Builder()
            .url(request.url.toString())
            .method(request.method, null)

        for ((key, value) in request.requestHeaders) {
            Log.v("TW", "interceptRequest header:${key} – ${value}")
            if (key == "User-Agent" || key == "Origin" || key == "Referer" || key == "Sec-Fetch-Mode") {
                continue
            }
            newRequestBuilder.addHeader(key, value)
        }

        newRequestBuilder.header("User-Agent", "curl/7.64.1")
        val newRequest = newRequestBuilder.build()

        if (request.method.uppercase() == "OPTIONS") {
            Log.v("TW", "OPTIONS request triggered")
            return OptionsAllowResponse.build()
    
        }
        //-------------


        Log.v("TW", "exec request ${request.url}")
        client.newCall(newRequest).execute().use { response ->
            Log.v("TW", "response ${response.code} ${response.message}")
            val responseHeaders = response.headers.names()
                .associateWith { response.headers(it)?.joinToString() }
                .toMutableMap()

            upsertKeyValue(responseHeaders, "Access-Control-Allow-Origin", "*")
            upsertKeyValue(
                responseHeaders,
                "Access-Control-Allow-Methods",
                "GET, POST, OPTIONS"
            )

            val contentType = response.header("Content-Type", "text/plain")
            val contentEncoding = response.header("Content-Encoding", "utf-8")

            val inputStream = ByteArrayInputStream(response.body?.bytes())
            val reasonPhrase =
                response.message.ifEmpty { "OK" } // provide a default value if the message is null or empty

            return WebResourceResponse(
                contentType,
                contentEncoding,
                response.code,
                reasonPhrase,
                responseHeaders,
                inputStream
            )
        }
    } catch (e: Exception) {
        Log.e("WebViewRequestHandler", "Error in interceptWebRequest => Not intercepting", e)
        return null
    }
}

fun upsertKeyValue(
    responseHeaders: MutableMap<String, String?>,
    keyToChange: String,
    value: String
): MutableMap<String, String?> {
    val keyToChangeLower = keyToChange.lowercase()
    for (key in responseHeaders.keys) {
        if (key.lowercase() == keyToChangeLower) {
            // Reassign old key
            responseHeaders[key] = value
            // Done
            return responseHeaders
        }
    }
    responseHeaders[keyToChange] = value
    return responseHeaders
}

Upvotes: 0

Ollie
Ollie

Reputation: 29

A shorter version that helped me using c# with Xamarin is the following:

    public override WebResourceResponse ShouldInterceptRequest(WebView view, IWebResourceRequest request)
    {
        if (request.Url.SchemeSpecificPart.StartsWith("//<domain>/"))
        {
            request.RequestHeaders.Add("Access-Control-Allow-Origin", "*");
        }

        return base.ShouldInterceptRequest(view, request);
    }

This override is done for Android.Webkit.WebViewClient, where <domain> is the one that is blocked by CORS policy.

Upvotes: -2

davidgoli
davidgoli

Reputation: 2495

This is now possible as of Android API level 21. You can create an OPTIONS response like so:

public class OptionsAllowResponse {
    static final SimpleDateFormat formatter = new SimpleDateFormat("E, dd MMM yyyy kk:mm:ss", Locale.US);

    @TargetApi(21)
    static WebResourceResponse build() {
        Date date = new Date();
        final String dateString = formatter.format(date);

        Map<String, String> headers = new HashMap<String, String>() {{
            put("Connection", "close");
            put("Content-Type", "text/plain");
            put("Date", dateString + " GMT");
            put("Access-Control-Allow-Origin", /* your domain here */);
            put("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS");
            put("Access-Control-Max-Age", "600");
            put("Access-Control-Allow-Credentials", "true");
            put("Access-Control-Allow-Headers", "accept, authorization, Content-Type");
            put("Via", "1.1 vegur");
        }};

        return new WebResourceResponse("text/plain", "UTF-8", 200, "OK", headers, null);
    }
}

and then call it from your WebViewClient implementation as follows:

    @Override
    @TargetApi(21)
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        if (request.getMethod().equalsIgnoreCase("OPTIONS")) {
            return OptionsAllowResponse.build();
        }

        return null;
    }

This only works from API level 21, since the OPTIONS response requires inspecting the requested HTTP method from the WebResourceRequest, which is only available since API 21.

Upvotes: 21

pinco
pinco

Reputation: 891

AFAIK this is not possible, and believe me, I've tried many ways.

The best you can do is override resource loading. See Intercept and override HTTP-requests from WebView

Upvotes: 12

Related Questions