diff --git a/Chan/AndroidManifest.xml b/Chan/AndroidManifest.xml index 72f4436f..617c0f67 100644 --- a/Chan/AndroidManifest.xml +++ b/Chan/AndroidManifest.xml @@ -2,8 +2,8 @@ + android:versionCode="16" + android:versionName="v0.13" > Remove file File name Submit - Enter captcha + Enter the text Error sending reply Wrong captcha No file selected @@ -142,7 +142,14 @@ 4chan pass enables you to post without filling in CAPTCHAs Learn more https://www.4chan.org/pass - + Token + PIN + Log in + Log out + Connection error + Using 4chan pass + Off + Using 4chan pass diff --git a/Chan/res/xml/preference_pass.xml b/Chan/res/xml/preference_pass.xml index 3fc9175d..1108c3f5 100644 --- a/Chan/res/xml/preference_pass.xml +++ b/Chan/res/xml/preference_pass.xml @@ -1,10 +1,17 @@ - - + + + + + + diff --git a/Chan/src/org/floens/chan/chan/ChanUrls.java b/Chan/src/org/floens/chan/chan/ChanUrls.java index 298b14ff..b439307a 100644 --- a/Chan/src/org/floens/chan/chan/ChanUrls.java +++ b/Chan/src/org/floens/chan/chan/ChanUrls.java @@ -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"; + } } diff --git a/Chan/src/org/floens/chan/core/ChanPreferences.java b/Chan/src/org/floens/chan/core/ChanPreferences.java index 9e071491..596dda49 100644 --- a/Chan/src/org/floens/chan/core/ChanPreferences.java +++ b/Chan/src/org/floens/chan/core/ChanPreferences.java @@ -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", ""); + } } diff --git a/Chan/src/org/floens/chan/core/manager/ReplyManager.java b/Chan/src/org/floens/chan/core/manager/ReplyManager.java index 892e61af..f9eefce5 100644 --- a/Chan/src/org/floens/chan/core/manager/ReplyManager.java +++ b/Chan/src/org/floens/chan/core/manager/ReplyManager.java @@ -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); } } diff --git a/Chan/src/org/floens/chan/core/model/Pass.java b/Chan/src/org/floens/chan/core/model/Pass.java new file mode 100644 index 00000000..7a385b6b --- /dev/null +++ b/Chan/src/org/floens/chan/core/model/Pass.java @@ -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; + } +} diff --git a/Chan/src/org/floens/chan/core/model/Reply.java b/Chan/src/org/floens/chan/core/model/Reply.java index 003b0c93..bd6b88c7 100644 --- a/Chan/src/org/floens/chan/core/model/Reply.java +++ b/Chan/src/org/floens/chan/core/model/Reply.java @@ -17,4 +17,6 @@ public class Reply { public String captchaChallenge = ""; public String captchaResponse = ""; public String password = ""; + public boolean usePass = false; + public String passId = ""; } diff --git a/Chan/src/org/floens/chan/ui/activity/PassSettingsActivity.java b/Chan/src/org/floens/chan/ui/activity/PassSettingsActivity.java index 73e6aa9f..020b582c 100644 --- a/Chan/src/org/floens/chan/ui/activity/PassSettingsActivity.java +++ b/Chan/src/org/floens/chan/ui/activity/PassSettingsActivity.java @@ -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(); + } } } } diff --git a/Chan/src/org/floens/chan/ui/fragment/ReplyFragment.java b/Chan/src/org/floens/chan/ui/fragment/ReplyFragment.java index 6297fb9a..8f10c896 100644 --- a/Chan/src/org/floens/chan/ui/fragment/ReplyFragment.java +++ b/Chan/src/org/floens/chan/ui/fragment/ReplyFragment.java @@ -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(); diff --git a/Chan/src/org/floens/chan/ui/fragment/SettingsFragment.java b/Chan/src/org/floens/chan/ui/fragment/SettingsFragment.java index 5fc75e54..0df34124 100644 --- a/Chan/src/org/floens/chan/ui/fragment/SettingsFragment.java +++ b/Chan/src/org/floens/chan/ui/fragment/SettingsFragment.java @@ -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() { diff --git a/docs/pass.txt b/docs/pass.txt index 4cf2defa..ff2f5923 100644 --- a/docs/pass.txt +++ b/docs/pass.txt @@ -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 + + @@ -36,8 +45,12 @@ Error responses: 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;