/* * Overchan Android (Meta Imageboard Client) * Copyright (C) 2014-2016 miku-nyan <https://github.com/miku-nyan> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package nya.miku.wishmaster.http.cloudflare; import java.util.Date; import nya.miku.wishmaster.api.interfaces.CancellableTask; import nya.miku.wishmaster.common.Logger; import nya.miku.wishmaster.http.HttpConstants; import nya.miku.wishmaster.http.client.ExtendedHttpClient; import nya.miku.wishmaster.http.streamer.HttpRequestModel; import nya.miku.wishmaster.http.streamer.HttpResponseModel; import nya.miku.wishmaster.http.streamer.HttpStreamer; import nya.miku.wishmaster.lib.WebViewProxy; import nya.miku.wishmaster.ui.CompatibilityImpl; import cz.msebera.android.httpclient.HttpHost; import cz.msebera.android.httpclient.client.CookieStore; import cz.msebera.android.httpclient.client.HttpClient; import cz.msebera.android.httpclient.conn.params.ConnRouteParams; import cz.msebera.android.httpclient.cookie.Cookie; import cz.msebera.android.httpclient.cookie.SetCookie; import cz.msebera.android.httpclient.impl.cookie.BasicClientCookie; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.net.Uri; import android.os.Build; import android.view.View; import android.view.ViewGroup; import android.webkit.CookieManager; import android.webkit.CookieSyncManager; import android.webkit.WebView; import android.webkit.WebViewClient; /** * Выполнение Cloudflare-проверок * @author miku-nyan * */ @SuppressWarnings("deprecation") public class CloudflareChecker { private static final String TAG = "CloudflareChecker"; /** таймаут при анти-ддос проверке в милисекундах */ public static final long TIMEOUT = 35 * 1000; private CloudflareChecker() {} private static CloudflareChecker instance; /** Получить объект-синглтон */ public static synchronized CloudflareChecker getInstance() { if (instance == null) instance = new CloudflareChecker(); return instance; } /** Возвращает false, если в данный момент (какая-либо) анти-ддос уже выполняется */ public boolean isAvaibleAntiDDOS() { return !(processing || InterceptingAntiDDOS.getInstance().isProcessing()); } /** * Пройти анти-ддос проверку cloudflare * @param exception Cloudflare исключение * @param httpClient HTTP клиент * @param task отменяемая задача * @param activity активность, в контексте которого будет запущен WebView (webkit) * @return полученная cookie или null, если проверка не прошла по таймауту, или проверка уже проходит в другом потоке */ public Cookie checkAntiDDOS(CloudflareException exception, HttpClient httpClient, CancellableTask task, Activity activity) { if (exception.isRecaptcha()) throw new IllegalArgumentException(); HttpHost proxy = null; if (httpClient instanceof ExtendedHttpClient) { proxy = ((ExtendedHttpClient) httpClient).getProxy(); } else if (httpClient != null) { try { proxy = ConnRouteParams.getDefaultProxy(httpClient.getParams()); } catch (Exception e) { /*ignore*/ } } if (proxy != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { if (httpClient instanceof ExtendedHttpClient) { return InterceptingAntiDDOS.getInstance().check(exception, (ExtendedHttpClient) httpClient, task, activity); } else { throw new IllegalArgumentException( "cannot run anti-DDOS checking with proxy settings; http client is not instance of ExtendedHttpClient"); } } else { return checkAntiDDOS(exception, proxy, task, activity); } } private volatile boolean processing = false; private volatile boolean processing2 = false; private volatile Cookie currentCookie; private volatile WebView webView; private volatile Context webViewContext; private Object lock = new Object(); private Cookie checkAntiDDOS(final CloudflareException exception, final HttpHost proxy, CancellableTask task, final Activity activity) { synchronized (lock) { if (processing) return null; processing = true; } processing2 = true; currentCookie = null; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { CookieSyncManager.createInstance(activity); CookieManager.getInstance().removeAllCookie(); } else { CompatibilityImpl.clearCookies(CookieManager.getInstance()); } final ViewGroup layout = (ViewGroup) activity.getWindow().getDecorView().getRootView(); final WebViewClient client = new WebViewClient() { @Override public void onPageFinished(WebView webView, String url) { super.onPageFinished(webView, url); Logger.d(TAG, "Got Page: "+url); String value = null; try { String[] cookies = CookieManager.getInstance().getCookie(url).split("[;]"); for (String cookie : cookies) { if ((cookie != null) && (!cookie.trim().equals("")) && (cookie.startsWith(" " + exception.getRequiredCookieName() + "="))) { value = cookie.substring(exception.getRequiredCookieName().length() + 2); } } } catch (NullPointerException e) { Logger.e(TAG, e); } if (value != null) { BasicClientCookie cf_cookie = new BasicClientCookie(exception.getRequiredCookieName(), value); cf_cookie.setDomain("." + Uri.parse(url).getHost()); cf_cookie.setPath("/"); currentCookie = cf_cookie; Logger.d(TAG, "Cookie found: "+value); processing2 = false; } else { Logger.d(TAG, "Cookie is not found"); } } }; activity.runOnUiThread(new Runnable() { @SuppressLint("SetJavaScriptEnabled") @Override public void run() { webView = new WebView(activity); webView.setVisibility(View.GONE); layout.addView(webView); webView.setWebViewClient(client); webView.getSettings().setUserAgentString(HttpConstants.USER_AGENT_STRING); webView.getSettings().setJavaScriptEnabled(true); webViewContext = webView.getContext(); if (proxy != null) WebViewProxy.setProxy(webViewContext, proxy.getHostName(), proxy.getPort()); webView.loadUrl(exception.getCheckUrl()); } }); long startTime = System.currentTimeMillis(); while (processing2) { long time = System.currentTimeMillis() - startTime; if ((task != null && task.isCancelled()) || time > TIMEOUT) { processing2 = false; } } activity.runOnUiThread(new Runnable() { @Override public void run() { try { layout.removeView(webView); webView.stopLoading(); webView.clearCache(true); webView.destroy(); webView = null; } finally { if (proxy != null) WebViewProxy.setProxy(webViewContext, null, 0); processing = false; } } }); return currentCookie; } /** * Проверить рекапчу cloudflare, получить cookie * @param exception Cloudflare исключение * @param httpClient HTTP клиент * @param task отменяемая задача * @param challenge challenge рекапчи * @param recaptchaAnswer ответ на рекапчу * @return полученная cookie или null, если проверка не прошла */ public Cookie checkRecaptcha(CloudflareException exception, ExtendedHttpClient httpClient, CancellableTask task, String url) { if (!exception.isRecaptcha()) throw new IllegalArgumentException("wrong type of CloudflareException"); HttpResponseModel responseModel = null; try { HttpRequestModel rqModel = HttpRequestModel.builder().setGET().setNoRedirect(false).build(); CookieStore cookieStore = httpClient.getCookieStore(); removeCookie(cookieStore, exception.getRequiredCookieName()); responseModel = HttpStreamer.getInstance().getFromUrl(url, rqModel, httpClient, null, task); for (int i = 0; i < 3 && responseModel.statusCode == 400; ++i) { Logger.d(TAG, "HTTP 400"); responseModel.release(); responseModel = HttpStreamer.getInstance().getFromUrl(url, rqModel, httpClient, null, task); } for (Cookie cookie : cookieStore.getCookies()) { if (isClearanceCookie(cookie, url, exception.getRequiredCookieName())) { Logger.d(TAG, "Cookie found: " + cookie.getValue()); return cookie; } } Logger.d(TAG, "Cookie is not found"); } catch (Exception e) { Logger.e(TAG, e); } finally { if (responseModel != null) { responseModel.release(); } } return null; } static boolean isClearanceCookie(Cookie cookie, String url, String requiredCookieName) { try { String cookieName = cookie.getName(); String cookieDomain = cookie.getDomain(); if (!cookieDomain.startsWith(".")) { cookieDomain = "." + cookieDomain; } String urlCookie = "." + Uri.parse(url).getHost(); if (cookieName.equals(requiredCookieName) && cookieDomain.equalsIgnoreCase(urlCookie)) { return true; } } catch (Exception e) { Logger.e(TAG, e); } return false; } static void removeCookie(CookieStore store, String name) { boolean flag = false; for (Cookie cookie : store.getCookies()) { if (cookie.getName().equals(name)) { if (cookie instanceof SetCookie) { flag = true; ((SetCookie) cookie).setExpiryDate(new Date(0)); } else { Logger.e(TAG, "cannot remove cookie (object does not implement SetCookie): " + cookie.toString()); } } } if (flag) store.clearExpired(new Date()); } }