هنگامی که یک آدرس در webview لود می شود، دو نوع ریکوئست ممکن است در آن آدرس وجود داشته باشد:
گاهی اوقات نیاز داریم این ریکوئست ها را intercept کنیم و به آنها هدر اضافه کنیم؛ در این مقاله قصد دارم انواع مختلف این کار را از ابتدایی ترین روش تا پیشرفته ترین بررسی کرده و مزایا و معایب هرکدام را نیز بیان کنیم.
ابتدایی ترین روش برای این کار افزودن هدر در هنگام لود کردن یک آدرس در Webview می باشد:
val header = mutableMapOf<String,String>() header[TOKEN_KEY] = TOKEN webView.loadUrl(url,header)
این روش فقط به همان آدرس اولیه ای که webview لود می کند، هدر اضافه می کند و کاری به بقیه ریکوئست ها ندارد.
در روش قبل اگر بخواهیم برای همه دامنه ها یا آدرس هایی با مشخصه خاص intercept کنیم، میتوانیم این کار را در کلاینتی که برای webview تعریف میکنیم انجام دهیم.
class CustomViewClient:WebViewClient() { override fun shouldInterceptRequest(webView: WebView, request: WebResourceRequest): WebResourceResponse? { return if (request.url.toString().contains(SPECIFIC_REGEX)){ addCustomHeader(webView,request) }else super.shouldInterceptRequest(webView, request) } private fun addCustomHeader(webView: WebView, request: WebResourceRequest): WebResourceResponse? { return try { val okhttp: OkHttpClient = OkHttpClient.Builder().build() val okHttpRequest = Request.Builder().also { it.addHeader(TOKEN_KEY, TOKEN) it.url(request.url.toString()) } val response = okhttp.newCall(okHttpRequest.build()).execute() WebResourceResponse(UTF-8, response.body?.byteStream()) }catch (e:Exception){ super.shouldInterceptRequest(webView, request) } } }
سپس این فایل را در فرگمنت به عنوان کلاینت webview معرفی می کنیم:
webView.webViewClient = CustomViewClient()
این روش دو ایراد اساسی دارد:
همچنین بطور کلی ساز و کار تابع shouldInterceptRequest به این شکل است که ریکوئست ارسال می شود، زمانی که نتیجه آن آماده میشود قبل از ارسال جواب به webview، این تابع فراخوانی می شود و در صورتی که این تابع override نشده باشد و یا مقدار بازگشتی null باشد، اتفاقی نمیفتد در غیر اینصورت ابجکتی که از جنس که WebResourceResponse ساخته ایم را به webview می فرستد.
در روش قبلی اگر بخواهیم برای ریکوئست هایی که از نوع AJAX هستند، intercept را انجام دهیم، می توانیم قبل از این کار ابتدا نوع ریکوئست را تعیین کرده و سپس intercept کنیم.
class CustomViewClient:WebViewClient() { override fun shouldInterceptRequest(webView: WebView, request: WebResourceRequest): WebResourceResponse? { return if (getMimeType(request.url.toString()) == null){ addCustomHeader(webView,request) }else super.shouldInterceptRequest(webView, request) } private fun addCustomHeader(webView: WebView, request: WebResourceRequest): WebResourceResponse? { return try { val okhttp: OkHttpClient = OkHttpClient.Builder().build() val okHttpRequest = Request.Builder().also { it.addHeader(TOKEN_KEY, TOKEN) it.url(request.url.toString()) } val response = okhttp.newCall(okHttpRequest.build()).execute() WebResourceResponse(UTF-8, response.body?.byteStream()) }catch (e:Exception){ super.shouldInterceptRequest(webView, request) } } private fun getMimeType(url: String?): String? { var type: String? = null val extension = MimeTypeMap.getFileExtensionFromUrl(url) if (extension != null) { when (extension) { js -> { return text/javascript } woff -> { return application/font-woff } woff2 -> { return application/font-woff2 } ttf -> { return application/x-font-ttf } eot -> { return application/vnd.ms-fontobject } svg -> { return image/svg+xml } else -> type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) } } return type } }
اگر نوع بازگشتی تابع getMimeType از نوع null باشد، ریکوئست ما AJAX می باشد و می توانیم آن را intercept کنیم. سرعت این روش نسبت به روش های قبلی بیشتر است اما دو ایراد کلی دارد:
مشکل اول را می توان با استفاده لیستی شامل دامنه هایی که نباید intercept شوند مدیریت کرد؛ مشکل دوم را در روش بعدی توضیح می دهم.
تمامی کارهایی که در روش های قبلی در اپلیکیشن انجام شد، می تواند مستقیما از سمت Webview به وسیله جاوا اسکریپت انجام شود.
در ایتدا یک فایل جاوا اسکریپت در پوشه asset به نام دلخواه برای دریافت بدنه ریسپانس می سازیم؛ در اینجا نام آن را requestHeader.js می گذارم:
function shExpMatch(url, pattern) { pattern = pattern.replace(/\./g, '\\.'); pattern = pattern.replace(/\*/g, '.*'); pattern = pattern.replace(/\?/g, '.'); var newRe = new RegExp('^' + pattern + '$'); return newRe.test(url); } XMLHttpRequest.prototype.wrappedSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.origOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.setRequestHeader = function(header, value) { // Call the wrappedSetRequestHeader function first // so we get exceptions if we are in an erronous state etc. this.wrappedSetRequestHeader(header, value); // Create a headers map if it does not exist if (!this.headers) { this.headers = {}; } // Create a list for the header that if it does not exist if (!this.headers[header]) { this.headers[header] = []; } // Add the value to the header try { this.headers[header].push(value); } catch (e) {} } XMLHttpRequest.prototype.open = function(method, url, async, user, password) { this.recordedMethod = method; if (url != null && url != undefined) { if (url.toString().startsWith(http)) this.recordedUrl = url; else this.recordedUrl = .origin + url; } if (async == undefined) async = true; if (user == undefined) user = null; if (password == undefined) password = null; this.origOpen(method, url, async, user, password); }; XMLHttpRequest.prototype.origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function(body) { if (this.recordedUrl != null && this.recordedUrl != undefined) { for (const pattern of Object.keys(headerConfig.domainsExtraHeader)) { if (shExpMatch(this.recordedUrl, pattern)) { for (const header of headerConfig.domainsExtraHeader[pattern]) { if (headerConfig.headerData[header] != undefined && this.headers[header] == undefined) { this.setRequestHeader(header, headerConfig.headerData[header]); } } } } } this.origSend(body); }; const orgFetch = window.fetch; window.fetch = function(input, init) { if (!init) { init = {}; } if (!init.headers) { init.headers = new Headers(); } for (const pattern of Object.keys(headerConfig.domainsExtraHeader)) { if (shExpMatch(input, pattern)) { for (const header of headerConfig.domainsExtraHeader[pattern]) { if (init.headers instanceof Headers) { init.headers.append(header, headerConfig.headerData[header]); } else if (init.headers instanceof Array) { init.headers.push([header, headerConfig.headerData[header]]); } else { init.headers[header] = headerConfig.headerData[header]; } } } } return orgFetch(input, init); }
در کلاس بالا از متغیری به نام token، توکن ها را دریافت می کند ولی همانگونه که می بینید در این فایل چنین متغیری تعریف نشده است و باید در کلاس کلاینت این متغیر با سینتکس جاوا اسکریپت نوشته شود و به فایل بالا اضافه شده سپس به webview اینجکت شود:
class CustomViewClient( ):WebViewClient() { override fun Started(webView: WebView?, url: String?, favicon: Bitmap?) { val token = let token = {\n + \TOKEN_KEY\: \TOKEN\\n + } webView?.evaluateJavascript( ${token}\n${webView.context.assets.open(requestHeader.js).reader().readText()}, null ) super.onPageStarted(webView, url, favicon) } }
در این روش تمامی مشکلات روش های قبل برطرف شده و افزایش سرعت قابل توجهی را برای ما به همراه می آورد و باعث می شود نه نیاز به ریکوئست مجدد داشته باشیم و نه تشخیص نوع ریکوئست در سمت کلاینت.
البته باید به این نکته توجه کرد که اینجکت کردن جاوا اسکریپت به مرورگر از نظر امنیتی دارای ایراد است؛ البته باید این نکته را نیز در نظر گرفت که مثلا در روش دوم نیز ما با استفاده از okhttp می توانیم ریسپانس دلخواه خود را ساخته و به webview پاس بدهیم که این نیز خود، مشکل امنیتی است؛ این امکان تا کنون توسط مرورگر ها از جمله گوگل محدود نشده است اما اگر در ادامه این امکان از بین برود، باید دنبال راه های جایگزینی برای این کار بود.
برای اطلاعات بیشتر راجب مشکلات امنیتی که می تواند با استفاده از هرکدام از روش های بالا به وجود بیاید، می توانید لینک های زیر را مطالعه کنید: