/*
* 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.recaptcha;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import nya.miku.wishmaster.api.interfaces.CancellableTask;
import nya.miku.wishmaster.common.Async;
import nya.miku.wishmaster.common.IOUtils;
import nya.miku.wishmaster.common.Logger;
import nya.miku.wishmaster.common.MainApplication;
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 cz.msebera.android.httpclient.Header;
import cz.msebera.android.httpclient.HttpHeaders;
import cz.msebera.android.httpclient.HttpHost;
import cz.msebera.android.httpclient.client.HttpClient;
import cz.msebera.android.httpclient.conn.params.ConnRouteParams;
import cz.msebera.android.httpclient.message.BasicHeader;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
@SuppressWarnings("deprecation")
public class RecaptchaAjax {
private static final String TAG = "RecaptchaAjax";
private static final long TIMEOUT = 30 * 1000;
private static final String CUSTOM_UA = "Mozilla/5.0";
private static final String CHALLENGE_FILTER = "/recaptcha/api/image?c=";
private RecaptchaAjax() {}
static String getChallenge(String key, CancellableTask task, HttpClient httpClient, String scheme) throws Exception {
if (scheme == null) scheme = "http";
String address = scheme + "://127.0.0.1/";
String data = "<script type=\"text/javascript\"> " +
"var RecaptchaOptions = { " +
"theme : 'custom', " +
"custom_theme_widget: 'recaptcha_widget' " +
"}; " +
"</script>" +
"<div id=\"recaptcha_widget\" style=\"display:none\"> " +
"<div id=\"recaptcha_image\"></div> " +
"<input type=\"text\" id=\"recaptcha_response_field\" name=\"recaptcha_response_field\" /> " +
"</div>" +
"<script type=\"text/javascript\" src=\"" + scheme + "://www.google.com/recaptcha/api/challenge?k=" + key + "\"></script>";
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) {
return Intercepting.getInternal(address, data, task, httpClient);
} else {
return getChallengeInternal(address, data, task, proxy);
}
}
private static class Holder {
volatile WebView webView = null;
volatile String challenge = null;
}
private static String getChallengeFromImageUrl(String url) {
if (url.contains(CHALLENGE_FILTER)) {
String challenge = url.substring(url.indexOf(CHALLENGE_FILTER) + CHALLENGE_FILTER.length());
int index = challenge.indexOf('&');
if (index >= 0) challenge = challenge.substring(0, index);
if (challenge.length() > 0) return challenge;
}
return null;
}
private static String getChallengeInternal(final String address, final String data, CancellableTask task, final HttpHost proxy) throws Exception {
Logger.d(TAG, "not intercepting; proxy: " + (proxy == null ? "disabled" : "enabled"));
if (proxy != null) {
Logger.d(TAG, "AJAX recaptcha not using (proxy and old API)");
throw new Exception("proxy && old API");
//костыль с установкой прокси через reflection не используется, т.к. в отличие от js-antiddos, здесь не критично (получит noscript капчу)
}
final Context context = MainApplication.getInstance();
final Holder holder = new Holder();
Async.runOnUiThread(new Runnable() {
@SuppressLint("SetJavaScriptEnabled")
@Override
public void run() {
holder.webView = new WebView(context);
holder.webView.setWebViewClient(new WebViewClient() {
@Override
public void onLoadResource(WebView view, String url) {
String challenge = getChallengeFromImageUrl(url);
if (challenge != null) holder.challenge = challenge;
super.onLoadResource(view, url);
}
});
holder.webView.getSettings().setUserAgentString(CUSTOM_UA);
holder.webView.getSettings().setJavaScriptEnabled(true);
holder.webView.loadDataWithBaseURL(address, data, "text/html", "UTF-8", null);
}
});
long startTime = System.currentTimeMillis();
while (holder.challenge == null) {
long time = System.currentTimeMillis() - startTime;
if ((task != null && task.isCancelled()) || time > TIMEOUT) break;
Thread.yield();
}
Async.runOnUiThread(new Runnable() {
@Override
public void run() {
try {
holder.webView.stopLoading();
holder.webView.clearCache(true);
holder.webView.destroy();
} catch (Exception e) {
Logger.e(TAG, e);
}
}
});
if (holder.challenge == null) throw new RecaptchaException("couldn't get Recaptcha Challenge (AJAX)");
return holder.challenge;
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static class Intercepting {
private static String getInternal(final String url, final String data, final CancellableTask task, final HttpClient client) throws Exception {
Logger.d(TAG, "intercepting");
final Context context = MainApplication.getInstance();
final Holder holder = new Holder();
Header[] uaHeader = new Header[] { new BasicHeader(HttpHeaders.USER_AGENT, CUSTOM_UA) };
final HttpRequestModel rqModel = HttpRequestModel.builder().setGET().setCustomHeaders(uaHeader).build();
Async.runOnUiThread(new Runnable() {
@SuppressLint("SetJavaScriptEnabled")
@Override
public void run() {
holder.webView = new WebView(context);
holder.webView.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
String challenge = getChallengeFromImageUrl(url);
if (challenge != null) holder.challenge = challenge;
if (url.startsWith("http://127.0.0.1") || url.startsWith("https://127.0.0.1"))
return new WebResourceResponse("text/html", "UTF-8", new ByteArrayInputStream("127.0.0.1".getBytes()));
HttpResponseModel responseModel = null;
try {
responseModel = HttpStreamer.getInstance().getFromUrl(url, rqModel, client, 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, client, null, task);
}
BufOutputStream output = new BufOutputStream();
IOUtils.copyStream(responseModel.stream, output);
return new WebResourceResponse(null, null, output.toInputStream());
} catch (Exception e) {
Logger.e(TAG, e);
} finally {
if (responseModel != null) responseModel.release();
}
return new WebResourceResponse("text/html", "UTF-8", new ByteArrayInputStream("something wrong".getBytes()));
}
});
holder.webView.getSettings().setJavaScriptEnabled(true);
holder.webView.loadDataWithBaseURL(url, data, "text/html", "UTF-8", null);
}
});
long startTime = System.currentTimeMillis();
while (holder.challenge == null) {
long time = System.currentTimeMillis() - startTime;
if ((task != null && task.isCancelled()) || time > TIMEOUT) break;
Thread.yield();
}
Async.runOnUiThread(new Runnable() {
@Override
public void run() {
try {
holder.webView.stopLoading();
holder.webView.clearCache(true);
holder.webView.destroy();
} catch (Exception e) {
Logger.e(TAG, e);
}
}
});
if (holder.challenge == null) throw new RecaptchaException("couldn't get Recaptcha Challenge (AJAX-Intercept)");
return holder.challenge;
}
private static class BufOutputStream extends ByteArrayOutputStream {
public BufOutputStream() {
super(1024);
}
public InputStream toInputStream() {
return new ByteArrayInputStream(buf, 0, count);
}
}
}
}