Added support for 4chan pass.

captchafix
Florens Douwes 11 years ago
parent ae54ec6ae3
commit 3b48df7d8c
  1. 4
      Chan/AndroidManifest.xml
  2. 1
      Chan/res/layout/reply_captcha.xml
  3. 11
      Chan/res/values/strings.xml
  4. 19
      Chan/res/xml/preference_pass.xml
  5. 8
      Chan/src/org/floens/chan/chan/ChanUrls.java
  6. 18
      Chan/src/org/floens/chan/core/ChanPreferences.java
  7. 237
      Chan/src/org/floens/chan/core/manager/ReplyManager.java
  8. 11
      Chan/src/org/floens/chan/core/model/Pass.java
  9. 2
      Chan/src/org/floens/chan/core/model/Reply.java
  10. 83
      Chan/src/org/floens/chan/ui/activity/PassSettingsActivity.java
  11. 19
      Chan/src/org/floens/chan/ui/fragment/ReplyFragment.java
  12. 6
      Chan/src/org/floens/chan/ui/fragment/SettingsFragment.java
  13. 17
      docs/pass.txt

@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.floens.chan"
android:installLocation="auto"
android:versionCode="15"
android:versionName="v0.12" >
android:versionCode="16"
android:versionName="v0.13" >
<uses-sdk
android:minSdkVersion="14"

@ -6,6 +6,7 @@
android:padding="8dp" >
<TextView
android:id="@+id/reply_captcha_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/reply_captcha_tap_to_reload"

@ -75,7 +75,7 @@
<string name="reply_file_delete">Remove file</string>
<string name="reply_file_name">File name</string>
<string name="reply_submit">Submit</string>
<string name="reply_captcha">Enter captcha</string>
<string name="reply_captcha">Enter the text</string>
<string name="reply_error">Error sending reply</string>
<string name="reply_error_captcha">Wrong captcha</string>
<string name="reply_error_file">No file selected</string>
@ -142,7 +142,14 @@
<string name="pass_info_text">4chan pass enables you to post without filling in CAPTCHAs</string>
<string name="pass_info_learn_more">Learn more</string>
<string name="pass_info_link">https://www.4chan.org/pass</string>
<string name="pass_token">Token</string>
<string name="pass_pin">PIN</string>
<string name="pass_login">Log in</string>
<string name="pass_logout">Log out</string>
<string name="pass_error">Connection error</string>
<string name="pass_summary_enabled">Using 4chan pass</string>
<string name="pass_summary_disabled">Off</string>
<string name="pass_using">Using 4chan pass</string>
</resources>

@ -1,10 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
<CheckBoxPreference
android:key="dummy"
android:summaryOff="dummy off"
android:summaryOn="dummy on"
android:title="dummu" />
<EditTextPreference
android:key="preference_pass_token"
android:title="@string/pass_token" />
<EditTextPreference
android:key="preference_pass_pin"
android:title="@string/pass_pin"
android:inputType="numberPassword" />
<Preference
android:key="preference_pass_login"
android:title="@string/pass_login" />
</PreferenceScreen>

@ -39,14 +39,12 @@ public class ChanUrls {
return "https://a.4cdn.org/boards.json";
}
public static String getPostUrl(String board) {
public static String getReplyUrl(String board) {
return "https://sys.4chan.org/" + board + "/post";
// return "http://192.168.6.214/Testing/PostEchoer/post.php";
}
public static String getDeleteUrl(String board) {
return "https://sys.4chan.org/" + board + "/imgboard.php";
// return "http://192.168.6.214/Testing/PostEchoer/post.php";
}
public static String getBoardUrlDesktop(String board) {
@ -60,4 +58,8 @@ public class ChanUrls {
public static String getCatalogUrlDesktop(String board) {
return "https://boards.4chan.org/" + board + "/catalog";
}
public static String getPassUrl() {
return "https://sys.4chan.org/auth";
}
}

@ -64,8 +64,24 @@ public class ChanPreferences {
}
public static void setPassEnabled(boolean enabled) {
if (getWatchEnabled() != enabled) {
if (getPassEnabled() != enabled) {
ChanApplication.getPreferences().edit().putBoolean("preference_pass_enabled", enabled).commit();
}
}
public static String getPassToken() {
return ChanApplication.getPreferences().getString("preference_pass_token", "");
}
public static String getPassPin() {
return ChanApplication.getPreferences().getString("preference_pass_pin", "");
}
public static void setPassId(String id) {
ChanApplication.getPreferences().edit().putString("preference_pass_id", id).commit();
}
public static String getPassId() {
return ChanApplication.getPreferences().getString("preference_pass_id", "");
}
}

@ -9,7 +9,9 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.floens.chan.ChanApplication;
import org.floens.chan.R;
import org.floens.chan.chan.ChanUrls;
import org.floens.chan.core.model.Pass;
import org.floens.chan.core.model.Reply;
import org.floens.chan.core.model.SavedReply;
import org.floens.chan.ui.activity.ImagePickActivity;
@ -18,6 +20,8 @@ import org.floens.chan.utils.Utils;
import android.content.Context;
import android.content.Intent;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HeaderElement;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
import ch.boye.httpclientandroidlib.client.methods.HttpPost;
@ -127,6 +131,12 @@ public class ReplyManager {
fileListener = null;
}
public static abstract class FileListener {
public abstract void onFile(String name, File file);
public abstract void onFileLoading();
}
/**
* Get the CAPTCHA challenge hash from an JSON response.
*
@ -145,6 +155,95 @@ public class ReplyManager {
}
}
public void sendPass(Pass pass, final PassListener listener) {
Logger.i(TAG, "Sending pass login request");
HttpPost httpPost = new HttpPost(ChanUrls.getPassUrl());
MultipartEntity entity = new MultipartEntity();
try {
entity.addPart("act", new StringBody("do_login"));
entity.addPart("id", new StringBody(pass.token));
entity.addPart("pin", new StringBody(pass.pin));
// entity.addPart("pwd", new StringBody(reply.password));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return;
}
httpPost.setEntity(entity);
sendHttpPost(httpPost, new HttpPostSendListener() {
@Override
public void onReponse(String responseString, DefaultHttpClient client, HttpResponse response) {
PassResponse e = new PassResponse();
if (responseString == null || response == null) {
e.isError = true;
e.message = context.getString(R.string.pass_error);
} else {
e.responseData = responseString;
if (responseString.contains("Your device is now authorized")) {
e.message = "Success! Your device is now authorized.";
String passId = null;
Header[] cookieHeaders = response.getHeaders("Set-Cookie");
if (cookieHeaders != null) {
for (Header cookieHeader : cookieHeaders) {
HeaderElement[] elements = cookieHeader.getElements();
if (elements != null) {
for (HeaderElement el : elements) {
if (el != null) {
if (el.getName().equals("pass_id")) {
passId = el.getValue();
}
}
}
}
}
}
if (passId != null) {
e.passId = passId;
} else {
e.isError = true;
e.message = "Could not get pass id";
}
} else {
e.isError = true;
if (responseString.contains("Your Token must be exactly 10 characters")) {
e.message = "Incorrect token";
} else if (responseString.contains("You have left one or more fields blank")) {
e.message = "You have left one or more fields blank";
} else if (responseString.contains("Incorrect Token or PIN")) {
e.message = "Incorrect Token or PIN";
} else {
e.message = "Unknown error";
}
}
}
listener.onResponse(e);
}
});
}
public static interface PassListener {
public void onResponse(PassResponse response);
}
public static class PassResponse {
public boolean isError = false;
public String responseData = "";
public String message = "";
public String passId;
}
/**
* Send an reply off to the server.
*
@ -181,7 +280,7 @@ public class ReplyManager {
sendHttpPost(httpPost, new HttpPostSendListener() {
@Override
public void onReponse(String responseString) {
public void onReponse(String responseString, DefaultHttpClient client, HttpResponse response) {
DeleteResponse e = new DeleteResponse();
if (responseString == null) {
@ -208,6 +307,20 @@ public class ReplyManager {
});
}
public static interface DeleteListener {
public void onResponse(DeleteResponse response);
}
public static class DeleteResponse {
public boolean isNetworkError = false;
public boolean isUserError = false;
public boolean isInvalidPassword = false;
public boolean isTooSoonError = false;
public boolean isTooOldError = false;
public boolean isSuccessful = false;
public String responseData = "";
}
/**
* Send an reply off to the server.
*
@ -220,7 +333,7 @@ public class ReplyManager {
public void sendReply(final Reply reply, final ReplyListener listener) {
Logger.i(TAG, "Sending reply request: " + reply.board + ", " + reply.resto);
HttpPost httpPost = new HttpPost(ChanUrls.getPostUrl(reply.board));
HttpPost httpPost = new HttpPost(ChanUrls.getReplyUrl(reply.board));
MultipartEntity entity = new MultipartEntity();
@ -243,6 +356,10 @@ public class ReplyManager {
entity.addPart("mode", new StringBody("regist"));
entity.addPart("pwd", new StringBody(reply.password));
if (reply.usePass) {
httpPost.addHeader("Cookie", "pass_id=" + reply.passId);
}
if (reply.file != null) {
entity.addPart("upfile", new FileBody(reply.file, reply.fileName, "application/octet-stream", "UTF-8"));
}
@ -255,7 +372,7 @@ public class ReplyManager {
sendHttpPost(httpPost, new HttpPostSendListener() {
@Override
public void onReponse(String responseString) {
public void onReponse(String responseString, DefaultHttpClient client, HttpResponse response) {
ReplyResponse e = new ReplyResponse();
if (responseString == null) {
@ -299,6 +416,43 @@ public class ReplyManager {
});
}
public static interface ReplyListener {
public void onResponse(ReplyResponse response);
}
public static class ReplyResponse {
/**
* No response from server.
*/
public boolean isNetworkError = false;
/**
* Some user error, like no file or captcha wrong.
*/
public boolean isUserError = false;
/**
* The userError was an fileError
*/
public boolean isFileError = false;
/**
* The userError was an captchaError
*/
public boolean isCaptchaError = false;
/**
* Received 'post successful'
*/
public boolean isSuccessful = false;
/**
* Raw html from the response. Used to set html in an WebView to the
* client, when the error was not recognized by Chan.
*/
public String responseData = "";
}
/**
* Async task to send an reply to the server. Uses HttpClient. Since Android
* 4.4 there is an updated version of HttpClient, 4.2, given with Android.
@ -315,12 +469,12 @@ public class ReplyManager {
HttpConnectionParams.setConnectionTimeout(httpParameters, POST_TIMEOUT);
HttpConnectionParams.setSoTimeout(httpParameters, POST_TIMEOUT);
DefaultHttpClient client = new DefaultHttpClient(httpParameters);
final DefaultHttpClient client = new DefaultHttpClient(httpParameters);
String responseString = null;
HttpResponse response = null;
try {
HttpResponse response = client.execute(post);
response = client.execute(post);
responseString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (ClientProtocolException e) {
e.printStackTrace();
@ -329,11 +483,12 @@ public class ReplyManager {
}
final String finalResponseString = responseString;
final HttpResponse finalResponse = response;
Utils.runOnUiThread(new Runnable() {
@Override
public void run() {
listener.onReponse(finalResponseString);
listener.onReponse(finalResponseString, client, finalResponse);
}
});
}
@ -341,72 +496,6 @@ public class ReplyManager {
}
private static interface HttpPostSendListener {
public void onReponse(String responseString);
}
public static abstract class FileListener {
/**
* When the file was picked
*
* @param name
* @param file
*/
public abstract void onFile(String name, File file);
/**
* When the file has started loading.
*/
public abstract void onFileLoading();
}
public static interface DeleteListener {
public void onResponse(DeleteResponse response);
}
public static class DeleteResponse {
public boolean isNetworkError = false;
public boolean isUserError = false;
public boolean isInvalidPassword = false;
public boolean isTooSoonError = false;
public boolean isTooOldError = false;
public boolean isSuccessful = false;
public String responseData = "";
}
public static interface ReplyListener {
public void onResponse(ReplyResponse response);
}
public static class ReplyResponse {
/**
* No response from server.
*/
public boolean isNetworkError = false;
/**
* Some user error, like no file or captcha wrong.
*/
public boolean isUserError = false;
/**
* The userError was an fileError
*/
public boolean isFileError = false;
/**
* The userError was an captchaError
*/
public boolean isCaptchaError = false;
/**
* Received 'post successful'
*/
public boolean isSuccessful = false;
/**
* Raw html from the response. Used to set html in an WebView to the
* client, when the error was not recognized by Chan.
*/
public String responseData = "";
public void onReponse(String responseString, DefaultHttpClient client, HttpResponse response);
}
}

@ -0,0 +1,11 @@
package org.floens.chan.core.model;
public class Pass {
public String token = "";
public String pin = "";
public Pass(String token, String pin) {
this.token = token;
this.pin = pin;
}
}

@ -17,4 +17,6 @@ public class Reply {
public String captchaChallenge = "";
public String captchaResponse = "";
public String password = "";
public boolean usePass = false;
public String passId = "";
}

@ -1,16 +1,22 @@
package org.floens.chan.ui.activity;
import org.floens.chan.ChanApplication;
import org.floens.chan.R;
import org.floens.chan.core.ChanPreferences;
import org.floens.chan.core.manager.ReplyManager;
import org.floens.chan.core.manager.ReplyManager.PassResponse;
import org.floens.chan.core.model.Pass;
import org.floens.chan.utils.Utils;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
@ -21,15 +27,35 @@ import android.widget.Switch;
import android.widget.TextView;
public class PassSettingsActivity extends Activity implements OnCheckedChangeListener {
private static PassSettingsActivity instance;
private Switch enableSwitch;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
instance = this;
setFragment(ChanPreferences.getPassEnabled());
}
@Override
protected void onDestroy() {
super.onDestroy();
instance = null;
}
@Override
public void onBackPressed() {
super.onBackPressed();
if (TextUtils.isEmpty(ChanPreferences.getPassId())) {
ChanPreferences.setPassEnabled(false);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.action_bar_switch, menu);
@ -51,16 +77,7 @@ public class PassSettingsActivity extends Activity implements OnCheckedChangeLis
private void setSwitch(boolean enabled) {
enableSwitch.setChecked(enabled);
ChanPreferences.setPassEnabled(enabled);
enableSwitch.setEnabled(false);
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
enableSwitch.setEnabled(true);
}
}, 500);
}
private void setFragment(boolean enabled) {
@ -100,6 +117,50 @@ public class PassSettingsActivity extends Activity implements OnCheckedChangeLis
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preference_pass);
Preference login = findPreference("preference_pass_login");
login.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
if (PassSettingsActivity.instance != null) {
Pass pass = new Pass(ChanPreferences.getPassToken(), ChanPreferences.getPassPin());
onLoginClick(pass);
}
return true;
}
});
updateLoginButton();
}
private void updateLoginButton() {
findPreference("preference_pass_login").setTitle(TextUtils.isEmpty(ChanPreferences.getPassId()) ? R.string.pass_login : R.string.pass_logout);
}
private void onLoginClick(Pass pass) {
if (TextUtils.isEmpty(ChanPreferences.getPassId())) {
// Login
final ProgressDialog dialog = ProgressDialog.show(getActivity(), null, "Logging in");
ChanApplication.getReplyManager().sendPass(pass, new ReplyManager.PassListener() {
@Override
public void onResponse(PassResponse response) {
dialog.dismiss();
if (getActivity() == null)
return;
new AlertDialog.Builder(getActivity()).setMessage(response.message)
.setNeutralButton(R.string.ok, null).create().show();
ChanPreferences.setPassId(response.passId);
updateLoginButton();
}
});
} else {
// Logout
ChanPreferences.setPassId("");
updateLoginButton();
}
}
}
}

@ -74,7 +74,7 @@ public class ReplyFragment extends DialogFragment {
private EditText fileNameView;
private LoadView imageViewContainer;
private LoadView captchaContainer;
private TextView captchaText;
private TextView captchaInput;
private LoadView responseContainer;
private Activity context;
@ -207,7 +207,13 @@ public class ReplyFragment extends DialogFragment {
getCaptcha();
}
});
captchaText = (TextView) container.findViewById(R.id.reply_captcha);
captchaInput = (TextView) container.findViewById(R.id.reply_captcha);
if (ChanPreferences.getPassEnabled()) {
((TextView) container.findViewById(R.id.reply_captcha_text)).setText(R.string.pass_using);
container.findViewById(R.id.reply_captcha_container).setVisibility(View.GONE);
container.findViewById(R.id.reply_captcha).setVisibility(View.GONE);
}
cancelButton = (Button) container.findViewById(R.id.reply_cancel);
cancelButton.setOnClickListener(new OnClickListener() {
@ -431,7 +437,7 @@ public class ReplyFragment extends DialogFragment {
draft.subject = subjectView.getText().toString();
draft.comment = commentView.getText().toString();
draft.captchaChallenge = captchaChallenge;
draft.captchaResponse = captchaText.getText().toString();
draft.captchaResponse = captchaInput.getText().toString();
draft.fileName = "image";
if (fileNameView != null) {
@ -444,6 +450,11 @@ public class ReplyFragment extends DialogFragment {
draft.resto = loadable.isBoardMode() ? -1 : loadable.no;
draft.board = loadable.board;
if (ChanPreferences.getPassEnabled()) {
draft.usePass = true;
draft.passId = ChanPreferences.getPassId();
}
ChanApplication.getReplyManager().sendReply(draft, new ReplyManager.ReplyListener() {
@Override
public void onResponse(ReplyResponse response) {
@ -470,7 +481,7 @@ public class ReplyFragment extends DialogFragment {
setClosable(true);
flipPage(1);
getCaptcha();
captchaText.setText("");
captchaInput.setText("");
} else if (response.isSuccessful) {
shouldSaveDraft = false;
Toast.makeText(context, R.string.reply_success, Toast.LENGTH_SHORT).show();

@ -81,6 +81,12 @@ public class SettingsFragment extends PreferenceFragment {
watchPreference.setSummary(ChanPreferences.getWatchEnabled() ? R.string.watch_summary_enabled
: R.string.watch_summary_disabled);
}
final Preference passPreference = findPreference("pass_settings");
if (passPreference != null) {
passPreference.setSummary(ChanPreferences.getPassEnabled() ? R.string.pass_summary_enabled
: R.string.pass_summary_disabled);
}
}
private void updateDeveloperPreference() {

@ -1,14 +1,23 @@
Authorizing pass:
POST https://sys.4chan.org/auth
act=do_login&id=HASH&pin=PIN
act=do_login&id=TOKEN&pin=PIN
(optional) long_login=yes
Login response:
Reset:
Set-Cookie:pass_id=0; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=-1396102928; path=/; domain=sys.4chan.org; secure; httponly
Cookie 1;
Set-Cookie:pass_id=PASS_ID; expires=Sun, 30-Mar-2014 14:22:09 GMT; Max-Age=86400; path=/; domain=.4chan.org; secure; httponly
Cookie 2:
Set-Cookie:pass_enabled=1; expires=Sun, 30-Mar-2014 14:22:09 GMT; Max-Age=86400; path=/; domain=.4chan.org
pass_id=ID
pass_enabled=1 // probably js only
<!DOCTYPE html>
<html>
<head>
@ -36,8 +45,12 @@ Error responses:
</body>
This Pass is already in use by another IP. Please wait 19 minutes and re-authorize by visiting this page again to change IPs.
Your Token must be exactly 10 characters.
Error: You have left one or more fields blank.
Incorrect Token or PIN.
Success! Your device is now authorized.
You can begin using your Pass immediately—just visit any board and start posting!
And when posting:
Cookie: pass_id=PASS_ID; pass_enabled=1;

Loading…
Cancel
Save