From 8fec9c894ec07d45c70f56c9c06ba0c66939ff5a Mon Sep 17 00:00:00 2001 From: k1rakishou Date: Sat, 18 May 2019 17:42:51 +0300 Subject: [PATCH] Add captchaInfo parsing tests Store real google cookie up to three months instead of until the next app restart --- Clover/app/build.gradle | 2 + .../chan/core/settings/ChanSettings.java | 2 + .../ui/captcha/v2/CaptchaNoJsHtmlParser.java | 6 +- .../ui/captcha/v2/CaptchaNoJsPresenterV2.java | 24 +++- .../BehaviourSettingsController.java | 5 +- Clover/app/src/main/res/values/strings.xml | 2 +- .../captcha/v2/CaptchaNoJsHtmlParserTest.java | 104 ++++++++++++++++++ 7 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 Clover/app/src/test/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParserTest.java diff --git a/Clover/app/build.gradle b/Clover/app/build.gradle index 8eb4f098..ad8ff1fd 100644 --- a/Clover/app/build.gradle +++ b/Clover/app/build.gradle @@ -158,4 +158,6 @@ dependencies { implementation 'org.codejargon.feather:feather:1.0' releaseImplementation 'ch.acra:acra-http:5.1.3' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:2.27.0' } diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java index d1789fff..cf2793c8 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java +++ b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java @@ -163,6 +163,7 @@ public class ChanSettings { public static final BooleanSetting useNewCaptchaWindow; public static final BooleanSetting useRealGoogleCookies; public static final StringSetting googleCookie; + public static final LongSetting lastGoogleCookieUpdateTime; static { SettingProvider p = new SharedPreferencesSettingProvider(AndroidUtils.getPreferences()); @@ -256,6 +257,7 @@ public class ChanSettings { useNewCaptchaWindow = new BooleanSetting(p, "use_new_captcha_window", true); useRealGoogleCookies = new BooleanSetting(p, "use_real_google_cookies", false); googleCookie = new StringSetting(p, "google_cookie", ""); + lastGoogleCookieUpdateTime = new LongSetting(p, "last_google_cookie_update_time", 0L); // Old (but possibly still in some users phone) // preference_board_view_mode default "list" diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java index bc22ae89..bd063398 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParser.java @@ -123,7 +123,7 @@ public class CaptchaNoJsHtmlParser { return token; } - private void parseChallengeTitle( + public void parseChallengeTitle( String responseHtml, CaptchaInfo captchaInfo ) throws CaptchaNoJsV2ParsingError { @@ -240,7 +240,7 @@ public class CaptchaNoJsHtmlParser { } } - private void parseCParameter( + public void parseCParameter( String responseHtml, CaptchaInfo captchaInfo ) throws CaptchaNoJsV2ParsingError { @@ -269,7 +269,7 @@ public class CaptchaNoJsHtmlParser { captchaInfo.setcParameter(cParameter); } - private void parseCheckboxes( + public void parseCheckboxes( String responseHtml, CaptchaInfo captchaInfo ) throws CaptchaNoJsV2ParsingError { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java index 8f391c1c..256ca2c1 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsPresenterV2.java @@ -31,6 +31,7 @@ import java.net.URLEncoder; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import okhttp3.Headers; @@ -48,7 +49,6 @@ public class CaptchaNoJsPresenterV2 { private static final String acceptHeader = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"; private static final String acceptEncodingHeader = "deflate, br"; private static final String acceptLanguageHeader = "en-US"; - private static final String recaptchaUrlBase = "https://www.google.com/recaptcha/api/fallback?k="; private static final String googleBaseUrl = "https://www.google.com/"; private static final String encoding = "UTF-8"; @@ -58,6 +58,7 @@ public class CaptchaNoJsPresenterV2 { private static final String setCookieHeaderName = "set-cookie"; private static final int SUCCESS_STATUS_CODE = 200; private static final long CAPTCHA_REQUEST_THROTTLE_MS = 3000L; + private static final long THREE_MONTHS = TimeUnit.DAYS.toMillis(90); // this cookie is taken from dashchan private static final String defaultGoogleCookies = "NID=87=gkOAkg09AKnvJosKq82kgnDnHj8Om2pLskKhdna02msog8HkdHDlasDf"; @@ -135,6 +136,8 @@ public class CaptchaNoJsPresenterV2 { String recaptchaUrl = recaptchaUrlBase + siteKey; RequestBody body = createResponseBody(prevCaptchaInfo, selectedIds); + Logger.d(TAG, "Verify called. Current cookie = " + googleCookie); + Request request = new Request.Builder() .url(recaptchaUrl) .post(body) @@ -282,11 +285,17 @@ public class CaptchaNoJsPresenterV2 { return defaultGoogleCookies; } - if (!forced && !googleCookie.isEmpty()) { + boolean isItTimeToUpdateCookies = + ((System.currentTimeMillis() - ChanSettings.lastGoogleCookieUpdateTime.get()) > THREE_MONTHS); + + if (!forced && (!googleCookie.isEmpty() && !isItTimeToUpdateCookies)) { Logger.d(TAG, "We already have google cookies"); return googleCookie; } + Logger.d(TAG, "Time to update cookies: forced = " + forced + ", isCookieEmpty = " + + googleCookie.isEmpty() + ", last cookie expired = " + isItTimeToUpdateCookies); + Request request = new Request.Builder() .url(googleBaseUrl) .header("User-Agent", userAgentHeader) @@ -297,9 +306,16 @@ public class CaptchaNoJsPresenterV2 { try (Response response = okHttpClient.newCall(request).execute()) { String newCookie = handleGetGoogleCookiesResponse(response); - ChanSettings.googleCookie.set(newCookie); + if (!newCookie.equalsIgnoreCase(defaultGoogleCookies)) { + ChanSettings.googleCookie.set(newCookie); + ChanSettings.lastGoogleCookieUpdateTime.set(System.currentTimeMillis()); + + Logger.d(TAG, "Successfully refreshed google cookies, new cookie = " + newCookie); + } else { + Logger.d(TAG, "Could not successfully handle google cookie response, " + + "using the default google cookies until the next request"); + } - Logger.d(TAG, "Successfully refreshed google cookies, new cookie = " + newCookie); return newCookie; } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java index 0a5ff7a9..e2f83be1 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/BehaviourSettingsController.java @@ -65,8 +65,11 @@ public class BehaviourSettingsController extends SettingsController { if (!ChanSettings.useNewCaptchaWindow.get()) { ChanSettings.useRealGoogleCookies.set(false); - // reset the old google cookie as well + // Reset the old google cookie ChanSettings.googleCookie.set(""); + + // and cookie update time as well + ChanSettings.lastGoogleCookieUpdateTime.set(0L); } rebuildPreferences(); diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index e568791f..93a1c29a 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -582,7 +582,7 @@ Don't have a 4chan Pass?
Captcha Use new captcha window for no-js captcha Use real google cookies instead of hardcoded ones - When the real google cookies a GET request to the google.com will be executed to get the google cookies (NID) which will be used for captcha authentication. Why would you need them? Because the hardcoded ones sometimes will make you re-enter the captcha dozens of times. Those cookies will be updated automatically (when posting the first time after the app start). But it will also be possible to update them manually if necessary (for example, when the old ones give too many challenges in a row). + When this option is enabled a GET request to the google.com will be executed to get the google cookies (NID) which will be used for captcha authentication. Why would you need them? Because the hardcoded ones sometimes will make you re-enter the captcha dozens of times. Those cookies will be updated automatically (every three months). But it will also be possible to update them manually if necessary (for example, when the old ones give too many challenges in a row). Verify Reload diff --git a/Clover/app/src/test/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParserTest.java b/Clover/app/src/test/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParserTest.java new file mode 100644 index 00000000..37a2f32a --- /dev/null +++ b/Clover/app/src/test/java/org/floens/chan/ui/captcha/v2/CaptchaNoJsHtmlParserTest.java @@ -0,0 +1,104 @@ +package org.floens.chan.ui.captcha.v2; + +import android.content.Context; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; + +import okhttp3.OkHttpClient; + +import static org.junit.Assert.assertEquals; + +public class CaptchaNoJsHtmlParserTest { + private List inputDataList = new ArrayList<>(); + private List> resultCheckboxList = new ArrayList<>(); + private List resultTitleList = new ArrayList<>(); + private List resultCParameterList = new ArrayList<>(); + + private Context contextMock = Mockito.mock(Context.class); + private OkHttpClient okHttpClient = Mockito.mock(OkHttpClient.class); + private CaptchaNoJsHtmlParser parser = new CaptchaNoJsHtmlParser(contextMock, okHttpClient); + + @Before + public void setUp() { + inputDataList.add("reCAPTCHA challenge
reCAPTCHA
\"reCAPTCHA
"); + inputDataList.add("reCAPTCHA challenge
reCAPTCHA
\"reCAPTCHA
"); + inputDataList.add("reCAPTCHA challenge
reCAPTCHA
\"reCAPTCHA
"); + inputDataList.add("reCAPTCHA challenge
reCAPTCHA
\"reCAPTCHA
"); + inputDataList.add("reCAPTCHA challenge
reCAPTCHA
\"reCAPTCHA
"); + inputDataList.add("reCAPTCHA challenge
reCAPTCHA
\"reCAPTCHA
"); + inputDataList.add("reCAPTCHA challenge
reCAPTCHA
\"reCAPTCHA
"); + + for (int i = 0; i < inputDataList.size(); ++i) { + resultCheckboxList.add(new ArrayList<>()); + + resultCheckboxList.get(i).add(0); + resultCheckboxList.get(i).add(1); + resultCheckboxList.get(i).add(2); + resultCheckboxList.get(i).add(3); + resultCheckboxList.get(i).add(4); + resultCheckboxList.get(i).add(5); + resultCheckboxList.get(i).add(6); + resultCheckboxList.get(i).add(7); + resultCheckboxList.get(i).add(8); + } + + resultTitleList.add("Select all images with cars"); + resultTitleList.add("Select all images with mountains or hills"); + resultTitleList.add("Select all images with crosswalks"); + resultTitleList.add("Select all images with bridges"); + resultTitleList.add("Select all images with bicycles"); + resultTitleList.add("Select all images with cars"); + resultTitleList.add("Select all images with a bus"); + + resultCParameterList.add("03AOLTBLSABiot1uRaxXzd4_PRTTGbxW4AV8NOI947nBNVIBwdfOC2DwB0gdrPnfe8YYLI3kkW_tzwTJbQwZk0_nKaIsfXLaGWnnrEhvRMstWDzRin27w_Yalo6OS8Yr5sIqYBUfI496UtsoDGSL8NVQAbxjXocz_E7DTNXVBMfdLPx093-9vUHKH4yt4Mo6HuKSXmBPC2KNL4j_yl78YQ_VG1mprDwUkQeW-MQZxBOQb4MyhWZMOmNp_aRgHKGpJYHpcVMljDUvwZRk_xLw-NoJAWsgCIAt6pfrUE9jvHxjE4fep8OCH2T-6g-_Lla_UovuyEVIfUi1MQa6KWQMypsavA2mVUjTvIe_NkkO8fJ6gteHqUc7a9FTIHHs1OR5uV7fD0SCn9G5ii"); + resultCParameterList.add("03AOLTBLRv_ZKMPWAc0bBYTnC9wPIQElGB6H6SrifMnrj1yDeFnUPNpOPgGZHdaGPVzfieFHzQOh4GIKWUBPQ_aMs3gp0EJmgdP-LAGFBZIreVB-boqP1DyIblN-3ws1bxCyVvw4kSanc08rPVr7G2nwy7b0kkBV82x8ypU1QP1fQR1HxC8qUddQZSLvgmkFiRatbaOqMdsMPDwl1ENJVrzTkX0sQGmfqnf2aisqRbSsiH0BrW1_wTLPjiGv1SW-FrBYWKJZBWP_FanbTftUuhKLaYTjABDD2mcBg-7-QEpFbaFqi7cwSA4Y-SsCiJg7y0DBL8PjsDSxOqoY_Upc5I-0GI3X21KJLJMLxq9tRkWZbsyhHXNyFzzM_4PfOIiGErbTMzICa596q7"); + resultCParameterList.add("03AOLTBLQdQO0rHKgYInXnNyPho7cEllKtboAokZsqQPHyE5VkAEXwvkvin540F_CAPIA872WSFqdlShtJvERZPgOP_DmuO4lAF7CxIRDeRzyMd2kKF-ivS3XADiISevh50q1qtolKOW6nhdktgo4JxW3nej539yRZxJ4X6SRde_29QsGgqtTUUB82CYWZ2vOa9yclvuMMLPTFfIIRQHy4DNQXqVOEKV0PfvBatjLhq4ekuYgEoHbni97ePTmjvfsL0gy6CR2SAmOf6m7oJm2snVyBBgxWfMnPue6wypmHco7JRLcEsRHOvUDBLa6rMT5p7fiRP3sxPQNcv_H6prK_EAtSPOB29SELUlf7G72UlcxBNOaKxsOY_xzyFHnyFPRTERh2xc7FEmDF"); + resultCParameterList.add("03AOLTBLTkdQd0zGmSjFw9OCiKz18VT-h4lleJaBwtNMSDPFsUSR_9qepA1uCUW_FNAju-tgYzTOTjmjf0wWmvfLlwKCxEsyMKfYobnJXXYcOHyAwaOJRtEOAwn2SSmVEzlE1DYZ89lFH7XB5rw6tGpSJRJQkvitoVK-aLg_gzmYxbYJPH_VK9ofkfeGfhnZ6bsXoz1vnnhHYOXm7apmKjDEk6NIxKqB_FHDFclemX8rr7liJBaTcJ98I20a9wXGinBUEEyupoQ5gdnoYqLMu_XIQaRYtdmRsCgtm9RorpkJZxaqEpqfusfpFZUntIXkXF31dmx0hL-B5DpA4uFOBI6UmR2oPLAA2LhStMgKtlnl1IYwyrmaeYUybV0rPl0F5jMAr3GZxEa7Ld"); + resultCParameterList.add("03AOLTBLTAhnX3O2lESLZYL9y7w8txPisktNhNpI-h3hZpULhz_-yMEId_7-NW8RGCMbZ6_W8CRH6LnL7Icxdw3nISINh3DGcldAmF8jojjG17gPIVvCCk1WdKfgYSkC_oAThQDESoAOz-VGAahHroLUNSnCm1tHmExy5tRX3EGi0xO3ep-1iMSGuMb_aoo3hxIuEZE4_J37RGoE--t2yoU4pKDxfbnHNYYfl48KgIb_qBzUt9O-gOF5P3oMM7G5HpMHjq3lraD0Ep19tiq5c_P-JO21V7QiJBB8XiwqgIHsEwGaLsm3J2NdQjLHot1xy8_A8x04ZTqHw-d_LnpBOTz8Jry5ym2K-zfkqkK1VtbJUnayZ-i-e5_ErxE3HTesMtzJ6I-ZBu2h8T"); + resultCParameterList.add("03AOLTBLSu6KJ0tMzrENsXJM2H5K8GDF_2PZOEYKew4UxN49JDmoXnrlOFLYbETkJfXyG-kXWdmiTIg3XgR8vVfIf8J4oR1DpEyi9HcPaV9kLNeq-0aK3P1Vw62wBm3OOhU_zeP49kiwmy4icv4ro9NQjfBxw1scZgYsj_R41sBDvlLkofoDMrWNzsAjwFAM2pmbRGKf6qThzQ6ijSAjHBAD_oIK4E4m7kgeUZyEvw9-2MN_8AJoGXtI_74LdvbkngnF1qqK5gIKYX05AyaI39M1-TXfT5Gq2ePu8XMKUR709pZPc22Pw9lni0a1I-UyfGMJJ1u0Pvh0HKO6atPREGFcXwvhOQsSHakAGuzHUE8V5ywUqpW3mNFE0xly-GK07BbGMicQZ-7F-9"); + resultCParameterList.add("03AOLTBLSe4m_kEnKDI7Zg9oYjJ28Y1XKwS7uZ10NlWRoq-2e5tLG3r47ClAG3F_pTLqQC_krxnHkt8UBIV5EfW5goPvQQQN2W8ll3YYiMjWM_ge1_Z9lk_7M5KCriFBFhmPMB1MtMfeXS5LpcekGDSq9J2O20zySzNHXZ7xkvx5vJ327ioNoDEizwInEaLMG5tMqaWthqZE_-DZlpI7n3v35Glt3hNuT_utgSlBBqt7yipMuy-dunICsXT3Oxojeb-zcvPk3SFsWyMcTI-354eV1PfA-7q0mOGIADYuavzciBKjfZEHrsR9su48sHj_AH8RG4b_8yBzmR7WAj7Va-EhZzxDnlGdfBVqUo5wVUyJxlZe6NWkNkBwiaisxhC8As0VJIY0u_ugms"); + } + + @Test + public void testCheckBoxParsing() throws CaptchaNoJsHtmlParser.CaptchaNoJsV2ParsingError { + for (int i = 0; i < inputDataList.size(); ++i) { + String inputData = inputDataList.get(i); + CaptchaInfo captchaInfo = new CaptchaInfo(); + parser.parseCheckboxes(inputData, captchaInfo); + + for (int j = 0; j < captchaInfo.checkboxes.size(); j++) { + Integer checkbox = captchaInfo.checkboxes.get(j); + assertEquals(checkbox, resultCheckboxList.get(i).get(j)); + } + } + } + + @Test + public void testTitleParsing() throws CaptchaNoJsHtmlParser.CaptchaNoJsV2ParsingError { + for (int i = 0; i < inputDataList.size(); ++i) { + String inputData = inputDataList.get(i); + CaptchaInfo captchaInfo = new CaptchaInfo(); + parser.parseChallengeTitle(inputData, captchaInfo); + + String title = captchaInfo.captchaTitle.getTitle(); + assertEquals(resultTitleList.get(i), title); + } + } + + @Test + public void testCValueParsing() throws CaptchaNoJsHtmlParser.CaptchaNoJsV2ParsingError { + for (int i = 0; i < inputDataList.size(); ++i) { + String inputData = inputDataList.get(i); + CaptchaInfo captchaInfo = new CaptchaInfo(); + parser.parseCParameter(inputData, captchaInfo); + + String cParameter = captchaInfo.cParameter; + assertEquals(resultCParameterList.get(i), cParameter); + } + } +} \ No newline at end of file