mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 12:53:55 +01:00
Moved the CSRF token client over into main commons code (#5471)
This commit is contained in:
parent
3d0e65c92c
commit
8b8eb84fae
18 changed files with 252 additions and 37 deletions
|
|
@ -2,7 +2,7 @@ package fr.free.nrw.commons.actions
|
|||
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import org.wikipedia.csrf.CsrfTokenClient
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
||||
|
||||
/**
|
||||
* This class acts as a Client to facilitate wiki page editing
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package fr.free.nrw.commons.actions
|
|||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF
|
||||
import io.reactivex.Observable
|
||||
import org.wikipedia.csrf.CsrfTokenClient
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
|
|
|||
|
|
@ -0,0 +1,247 @@
|
|||
package fr.free.nrw.commons.auth.csrf;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executors;
|
||||
import org.wikipedia.AppAdapter;
|
||||
import org.wikipedia.dataclient.Service;
|
||||
import org.wikipedia.dataclient.ServiceFactory;
|
||||
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
||||
import org.wikipedia.login.LoginClient;
|
||||
import org.wikipedia.login.LoginResult;
|
||||
import org.wikipedia.util.log.L;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class CsrfTokenClient {
|
||||
private static final String ANON_TOKEN = "+\\";
|
||||
private static final int MAX_RETRIES = 1;
|
||||
private static final int MAX_RETRIES_OF_LOGIN_BLOCKING = 2;
|
||||
@NonNull private final WikiSite csrfWikiSite;
|
||||
@NonNull private final WikiSite loginWikiSite;
|
||||
private int retries = 0;
|
||||
|
||||
@Nullable private Call<MwQueryResponse> csrfTokenCall;
|
||||
@NonNull private LoginClient loginClient = new LoginClient();
|
||||
|
||||
public CsrfTokenClient(@NonNull WikiSite csrfWikiSite, @NonNull WikiSite loginWikiSite) {
|
||||
this.csrfWikiSite = csrfWikiSite;
|
||||
this.loginWikiSite = loginWikiSite;
|
||||
}
|
||||
|
||||
public void request(@NonNull final Callback callback) {
|
||||
request(false, callback);
|
||||
}
|
||||
|
||||
public void request(boolean forceLogin, @NonNull final Callback callback) {
|
||||
cancel();
|
||||
if (forceLogin) {
|
||||
retryWithLogin(new RuntimeException("Forcing login..."), callback);
|
||||
return;
|
||||
}
|
||||
csrfTokenCall = request(ServiceFactory.get(csrfWikiSite), callback);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
loginClient.cancel();
|
||||
if (csrfTokenCall != null) {
|
||||
csrfTokenCall.cancel();
|
||||
csrfTokenCall = null;
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@NonNull
|
||||
Call<MwQueryResponse> request(@NonNull Service service, @NonNull final Callback cb) {
|
||||
return requestToken(service, new CsrfTokenClient.Callback() {
|
||||
@Override public void success(@NonNull String token) {
|
||||
if (AppAdapter.get().isLoggedIn() && token.equals(ANON_TOKEN)) {
|
||||
retryWithLogin(new RuntimeException("App believes we're logged in, but got anonymous token."), cb);
|
||||
} else {
|
||||
cb.success(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void failure(@NonNull Throwable caught) {
|
||||
retryWithLogin(caught, cb);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void twoFactorPrompt() {
|
||||
cb.twoFactorPrompt();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void retryWithLogin(@NonNull Throwable caught, @NonNull final Callback callback) {
|
||||
if (retries < MAX_RETRIES
|
||||
&& !TextUtils.isEmpty(AppAdapter.get().getUserName())
|
||||
&& !TextUtils.isEmpty(AppAdapter.get().getPassword())) {
|
||||
retries++;
|
||||
|
||||
SharedPreferenceCookieManager.getInstance().clearAllCookies();
|
||||
|
||||
login(AppAdapter.get().getUserName(), AppAdapter.get().getPassword(), () -> {
|
||||
L.i("retrying...");
|
||||
request(callback);
|
||||
}, callback);
|
||||
} else {
|
||||
callback.failure(caught);
|
||||
}
|
||||
}
|
||||
|
||||
private void login(@NonNull final String username, @NonNull final String password,
|
||||
@NonNull final RetryCallback retryCallback,
|
||||
@NonNull final Callback callback) {
|
||||
new LoginClient().request(loginWikiSite, username, password,
|
||||
new LoginClient.LoginCallback() {
|
||||
@Override
|
||||
public void success(@NonNull LoginResult loginResult) {
|
||||
if (loginResult.pass()) {
|
||||
AppAdapter.get().updateAccount(loginResult);
|
||||
retryCallback.retry();
|
||||
} else {
|
||||
callback.failure(new LoginClient.LoginFailedException(loginResult.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token) {
|
||||
callback.twoFactorPrompt();
|
||||
}
|
||||
|
||||
@Override public void passwordResetPrompt(@Nullable String token) {
|
||||
// Should not happen here, but call the callback just in case.
|
||||
callback.failure(new LoginClient.LoginFailedException("Logged in with temporary password."));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(@NonNull Throwable caught) {
|
||||
callback.failure(caught);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull public String getTokenBlocking() throws Throwable {
|
||||
String token = "";
|
||||
Service service = ServiceFactory.get(csrfWikiSite);
|
||||
|
||||
for (int retry = 0; retry < MAX_RETRIES_OF_LOGIN_BLOCKING; retry++) {
|
||||
try {
|
||||
if (retry > 0) {
|
||||
// Log in explicitly
|
||||
new LoginClient().loginBlocking(loginWikiSite, AppAdapter.get().getUserName(),
|
||||
AppAdapter.get().getPassword(), "");
|
||||
}
|
||||
|
||||
// Get CSRFToken response off the main thread.
|
||||
Response<MwQueryResponse> response = Executors.newSingleThreadExecutor().submit(new CsrfTokenCallExecutor(service)).get();
|
||||
|
||||
if (response.body() == null || response.body().query() == null
|
||||
|| TextUtils.isEmpty(response.body().query().csrfToken())) {
|
||||
continue;
|
||||
}
|
||||
token = response.body().query().csrfToken();
|
||||
if (AppAdapter.get().isLoggedIn() && token.equals(ANON_TOKEN)) {
|
||||
throw new RuntimeException("App believes we're logged in, but got anonymous token.");
|
||||
}
|
||||
break;
|
||||
} catch (Throwable t) {
|
||||
L.w(t);
|
||||
}
|
||||
}
|
||||
if (TextUtils.isEmpty(token) || token.equals(ANON_TOKEN)) {
|
||||
throw new IOException("Invalid token, or login failure.");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
@VisibleForTesting @NonNull Call<MwQueryResponse> requestToken(@NonNull Service service,
|
||||
@NonNull final Callback cb) {
|
||||
Call<MwQueryResponse> call = service.getCsrfTokenCall();
|
||||
call.enqueue(new retrofit2.Callback<MwQueryResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<MwQueryResponse> call, @NonNull Response<MwQueryResponse> response) {
|
||||
if (call.isCanceled()) {
|
||||
return;
|
||||
}
|
||||
cb.success(response.body().query().csrfToken());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<MwQueryResponse> call, @NonNull Throwable t) {
|
||||
if (call.isCanceled()) {
|
||||
return;
|
||||
}
|
||||
cb.failure(t);
|
||||
}
|
||||
});
|
||||
return call;
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void success(@NonNull String token);
|
||||
void failure(@NonNull Throwable caught);
|
||||
void twoFactorPrompt();
|
||||
}
|
||||
|
||||
public static class DefaultCallback implements Callback {
|
||||
@Override
|
||||
public void success(@NonNull String token) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failure(@NonNull Throwable caught) {
|
||||
L.e(caught);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void twoFactorPrompt() {
|
||||
// TODO:
|
||||
}
|
||||
}
|
||||
|
||||
private interface RetryCallback {
|
||||
void retry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Class CsrfTokenCallExecutor which implement callable interface to get CsrfTokenCall.
|
||||
*/
|
||||
class CsrfTokenCallExecutor implements Callable<Response<MwQueryResponse>> {
|
||||
|
||||
/**
|
||||
* Service for token call.
|
||||
*/
|
||||
private Service service;
|
||||
|
||||
/**
|
||||
* Default Constructor.
|
||||
* @param service
|
||||
*/
|
||||
public CsrfTokenCallExecutor(Service service){
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a result, or throws an exception if unable to do so.
|
||||
*
|
||||
* @return computed result
|
||||
* @throws Exception if unable to compute a result
|
||||
*/
|
||||
@Override
|
||||
public Response<MwQueryResponse> call() throws Exception {
|
||||
return service.getCsrfTokenCall().execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ import okhttp3.HttpUrl;
|
|||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
import okhttp3.logging.HttpLoggingInterceptor.Level;
|
||||
import org.wikipedia.csrf.CsrfTokenClient;
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
|
||||
import org.wikipedia.dataclient.Service;
|
||||
import org.wikipedia.dataclient.ServiceFactory;
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import fr.free.nrw.commons.notification.models.Notification
|
|||
import fr.free.nrw.commons.notification.models.NotificationType
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import org.wikipedia.csrf.CsrfTokenClient
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse
|
||||
import org.wikipedia.util.DateUtil
|
||||
import javax.inject.Inject
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import javax.inject.Singleton;
|
|||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import org.wikipedia.csrf.CsrfTokenClient;
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
|
||||
import org.wikipedia.dataclient.mwapi.MwException;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import io.reactivex.Observable;
|
|||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import org.wikipedia.csrf.CsrfTokenClient;
|
||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
|
||||
import org.wikipedia.dataclient.mwapi.MwPostResponse;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue