fixed crashes

This commit is contained in:
Mariotaku Lee 2016-07-12 09:22:33 +08:00
parent dc8d7bd891
commit 9593365fa5
4 changed files with 539 additions and 588 deletions

View File

@ -351,7 +351,7 @@ public class MicroBlogAPIFactory implements TwidereConstants {
}
@NonNull
static String substituteLegacyApiBaseUrl(@NonNull String format, String domain) {
static String substituteLegacyApiBaseUrl(@NonNull String format, @Nullable String domain) {
final int idxOfSlash = format.indexOf("://");
// Not an url
if (idxOfSlash < 0) return format;
@ -362,8 +362,12 @@ public class MicroBlogAPIFactory implements TwidereConstants {
if (!host.equalsIgnoreCase("api.twitter.com")) return format;
final StringBuilder sb = new StringBuilder();
sb.append(format.substring(0, startOfHost));
sb.append(domain);
sb.append(".twitter.com");
if (domain != null) {
sb.append(domain);
sb.append(".twitter.com");
} else {
sb.append("twitter.com");
}
if (endOfHost != -1) {
sb.append(format.substring(endOfHost));
}

View File

@ -1,559 +0,0 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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 org.mariotaku.twidere.util;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.attoparser.AttoParseException;
import org.attoparser.IAttoHandler;
import org.attoparser.IAttoParser;
import org.attoparser.markup.MarkupAttoParser;
import org.attoparser.markup.html.AbstractStandardNonValidatingHtmlAttoHandler;
import org.attoparser.markup.html.HtmlParsingConfiguration;
import org.attoparser.markup.html.elements.IHtmlElement;
import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.twitter.TwitterOAuth;
import org.mariotaku.restfu.RestAPIFactory;
import org.mariotaku.restfu.RestClient;
import org.mariotaku.restfu.annotation.method.GET;
import org.mariotaku.restfu.annotation.method.POST;
import org.mariotaku.restfu.http.Endpoint;
import org.mariotaku.restfu.http.HttpRequest;
import org.mariotaku.restfu.http.HttpResponse;
import org.mariotaku.restfu.http.MultiValueMap;
import org.mariotaku.restfu.http.RestHttpClient;
import org.mariotaku.restfu.http.mime.FormBody;
import org.mariotaku.restfu.http.mime.SimpleBody;
import org.mariotaku.restfu.oauth.OAuthToken;
import org.mariotaku.restfu.okhttp3.OkHttpRestClient;
import org.mariotaku.twidere.util.net.SimpleCookieJar;
import java.io.IOException;
import java.io.Reader;
import java.net.URI;
import java.util.Map;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Response;
import static org.mariotaku.twidere.TwidereConstants.OAUTH_CALLBACK_OOB;
public class OAuthPasswordAuthenticator {
private static final IAttoParser PARSER = new MarkupAttoParser();
private final TwitterOAuth oauth;
private final RestHttpClient client;
private final Endpoint endpoint;
private final LoginVerificationCallback loginVerificationCallback;
private final String userAgent;
public OAuthPasswordAuthenticator(final TwitterOAuth oauth,
final LoginVerificationCallback loginVerificationCallback,
final String userAgent) {
final RestClient restClient = RestAPIFactory.getRestClient(oauth);
this.oauth = oauth;
this.endpoint = restClient.getEndpoint();
this.loginVerificationCallback = loginVerificationCallback;
this.userAgent = userAgent;
final OkHttpClient oldClient = ((OkHttpRestClient) restClient.getRestClient()).getClient();
final OkHttpClient.Builder builder = oldClient.newBuilder();
builder.cookieJar(new SimpleCookieJar());
builder.addNetworkInterceptor(new EndpointInterceptor(endpoint));
this.client = new OkHttpRestClient(builder.build());
}
public OAuthToken getOAuthAccessToken(final String username, final String password) throws AuthenticationException {
final OAuthToken requestToken;
try {
requestToken = oauth.getRequestToken(OAUTH_CALLBACK_OOB);
} catch (final MicroBlogException e) {
if (e.isCausedByNetworkIssue()) throw new AuthenticationException(e);
throw new AuthenticityTokenException(e);
}
try {
final AuthorizeRequestData authorizeRequestData = getAuthorizeRequestData(requestToken);
AuthorizeResponseData authorizeResponseData = getAuthorizeResponseData(requestToken,
authorizeRequestData, username, password);
if (!TextUtils.isEmpty(authorizeResponseData.oauthPin)) {
// Here we got OAuth PIN, just get access token directly
return oauth.getAccessToken(requestToken, authorizeResponseData.oauthPin);
} else if (authorizeResponseData.challenge == null) {
// No OAuth pin, or verification challenge, so treat as wrong password
throw new WrongUserPassException();
}
// Go to password verification flow
final String challengeType = authorizeResponseData.challenge.challengeType;
final String loginVerification = loginVerificationCallback.getLoginVerification(challengeType);
final AuthorizeRequestData verificationData = getVerificationData(authorizeResponseData,
loginVerification);
authorizeResponseData = getAuthorizeResponseData(requestToken,
verificationData, username, password);
if (TextUtils.isEmpty(authorizeResponseData.oauthPin)) {
throw new LoginVerificationException();
}
return oauth.getAccessToken(requestToken, authorizeResponseData.oauthPin);
} catch (final IOException | NullPointerException | MicroBlogException e) {
throw new AuthenticationException(e);
}
}
private AuthorizeRequestData getVerificationData(AuthorizeResponseData authorizeResponseData,
@Nullable String challengeResponse) throws IOException, LoginVerificationException {
HttpResponse response = null;
try {
final AuthorizeRequestData data = new AuthorizeRequestData();
final MultiValueMap<String> params = new MultiValueMap<>();
final AuthorizeResponseData.Verification verification = authorizeResponseData.challenge;
params.add("authenticity_token", verification.authenticityToken);
params.add("user_id", verification.userId);
params.add("challenge_id", verification.challengeId);
params.add("challenge_type", verification.challengeType);
params.add("platform", verification.platform);
params.add("redirect_after_login", verification.redirectAfterLogin);
final MultiValueMap<String> requestHeaders = new MultiValueMap<>();
requestHeaders.add("User-Agent", userAgent);
if (!TextUtils.isEmpty(challengeResponse)) {
params.add("challenge_response", challengeResponse);
}
final FormBody authorizationResultBody = new FormBody(params);
final HttpRequest.Builder authorizeResultBuilder = new HttpRequest.Builder();
authorizeResultBuilder.method(POST.METHOD);
authorizeResultBuilder.url(endpoint.construct("/account/login_verification"));
authorizeResultBuilder.headers(requestHeaders);
authorizeResultBuilder.body(authorizationResultBody);
response = client.newCall(authorizeResultBuilder.build()).execute();
parseAuthorizeRequestData(response, data);
if (TextUtils.isEmpty(data.authenticityToken)) {
throw new LoginVerificationException();
}
return data;
} catch (AttoParseException e) {
throw new LoginVerificationException("Login verification challenge failed", e);
} finally {
Utils.closeSilently(response);
}
}
private void parseAuthorizeRequestData(HttpResponse response, final AuthorizeRequestData data) throws AttoParseException, IOException {
final HtmlParsingConfiguration conf = new HtmlParsingConfiguration();
final IAttoHandler handler = new AbstractStandardNonValidatingHtmlAttoHandler(conf) {
boolean isOAuthFormOpened;
@Override
public void handleHtmlStandaloneElement(IHtmlElement element, boolean minimized,
String elementName, Map<String, String> attributes,
int line, int col) {
handleHtmlOpenElement(element, elementName, attributes, line, col);
handleHtmlCloseElement(element, elementName, line, col);
}
@Override
public void handleHtmlOpenElement(IHtmlElement element, String elementName,
Map<String, String> attributes, int line, int col) {
switch (elementName) {
case "form": {
if (attributes != null && "oauth_form".equals(attributes.get("id"))) {
isOAuthFormOpened = true;
}
break;
}
case "input": {
if (isOAuthFormOpened && attributes != null) {
final String name = attributes.get("name");
if (TextUtils.isEmpty(name)) break;
final String value = attributes.get("value");
if (name.equals("authenticity_token")) {
data.authenticityToken = value;
} else if (name.equals("redirect_after_login")) {
data.redirectAfterLogin = value;
}
}
break;
}
}
}
@Override
public void handleHtmlCloseElement(IHtmlElement element, String elementName, int line, int col) {
if ("form".equals(elementName)) {
isOAuthFormOpened = false;
}
}
};
PARSER.parse(SimpleBody.reader(response.getBody()), handler);
}
private AuthorizeResponseData getAuthorizeResponseData(OAuthToken requestToken,
AuthorizeRequestData authorizeRequestData,
String username, String password) throws IOException, AuthenticationException {
HttpResponse response = null;
try {
final AuthorizeResponseData data = new AuthorizeResponseData();
final MultiValueMap<String> params = new MultiValueMap<>();
params.add("oauth_token", requestToken.getOauthToken());
params.add("authenticity_token", authorizeRequestData.authenticityToken);
params.add("redirect_after_login", authorizeRequestData.redirectAfterLogin);
if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) {
params.add("session[username_or_email]", username);
params.add("session[password]", password);
}
final FormBody authorizationResultBody = new FormBody(params);
final MultiValueMap<String> requestHeaders = new MultiValueMap<>();
requestHeaders.add("User-Agent", userAgent);
data.referer = authorizeRequestData.referer;
final HttpRequest.Builder authorizeResultBuilder = new HttpRequest.Builder();
authorizeResultBuilder.method(POST.METHOD);
authorizeResultBuilder.url(endpoint.construct("/oauth/authorize"));
authorizeResultBuilder.headers(requestHeaders);
authorizeResultBuilder.body(authorizationResultBody);
response = client.newCall(authorizeResultBuilder.build()).execute();
final HtmlParsingConfiguration conf = new HtmlParsingConfiguration();
final IAttoHandler handler = new AbstractStandardNonValidatingHtmlAttoHandler(conf) {
boolean isOAuthPinDivOpened;
boolean isChallengeFormOpened;
@Override
public void handleHtmlStandaloneElement(IHtmlElement element, boolean minimized,
String elementName, Map<String, String> attributes,
int line, int col) {
handleHtmlOpenElement(element, elementName, attributes, line, col);
handleHtmlCloseElement(element, elementName, line, col);
}
@Override
public void handleHtmlCloseElement(IHtmlElement element, String elementName, int line, int col) {
switch (elementName) {
case "div": {
isOAuthPinDivOpened = false;
break;
}
case "form": {
isChallengeFormOpened = false;
break;
}
}
}
@Override
public void handleHtmlOpenElement(IHtmlElement element, String elementName,
Map<String, String> attributes, int line, int col) {
switch (elementName) {
case "div": {
if (attributes == null) break;
if ("oauth_pin".equals(attributes.get("id"))) {
isOAuthPinDivOpened = true;
}
break;
}
case "form": {
if (attributes == null) break;
final String id = attributes.get("id");
if (id == null) break;
switch (id) {
case "login-verification-form":
case "login-challenge-form": {
isChallengeFormOpened = true;
break;
}
}
break;
}
case "input":
if (attributes == null) break;
if (isChallengeFormOpened) {
final String name = attributes.get("name");
if (TextUtils.isEmpty(name)) break;
final String value = attributes.get("value");
switch (name) {
case "authenticity_token": {
ensureVerification();
data.challenge.authenticityToken = value;
break;
}
case "challenge_id": {
ensureVerification();
data.challenge.challengeId = value;
break;
}
case "challenge_type": {
ensureVerification();
data.challenge.challengeType = value;
break;
}
case "platform": {
ensureVerification();
data.challenge.platform = value;
break;
}
case "user_id": {
ensureVerification();
data.challenge.userId = value;
break;
}
case "redirect_after_login": {
ensureVerification();
data.challenge.redirectAfterLogin = value;
break;
}
}
}
break;
}
}
private void ensureVerification() {
if (data.challenge == null) {
data.challenge = new AuthorizeResponseData.Verification();
}
}
@Override
public void handleText(char[] buffer, int offset, int len, int line, int col) throws AttoParseException {
if (isOAuthPinDivOpened) {
final String s = new String(buffer, offset, len);
if (TextUtils.isDigitsOnly(s)) {
data.oauthPin = s;
}
}
}
};
PARSER.parse(SimpleBody.reader(response.getBody()), handler);
return data;
} catch (AttoParseException e) {
throw new AuthenticationException("Malformed HTML", e);
} finally {
Utils.closeSilently(response);
}
}
private AuthorizeRequestData getAuthorizeRequestData(OAuthToken requestToken) throws IOException,
AuthenticationException {
HttpResponse response = null;
try {
final AuthorizeRequestData data = new AuthorizeRequestData();
final HttpRequest.Builder authorizePageBuilder = new HttpRequest.Builder();
authorizePageBuilder.method(GET.METHOD);
authorizePageBuilder.url(endpoint.construct("/oauth/authorize", new String[]{"oauth_token",
requestToken.getOauthToken()}));
data.referer = Endpoint.constructUrl("https://api.twitter.com/oauth/authorize",
new String[]{"oauth_token", requestToken.getOauthToken()});
final MultiValueMap<String> requestHeaders = new MultiValueMap<>();
requestHeaders.add("User-Agent", userAgent);
authorizePageBuilder.headers(requestHeaders);
final HttpRequest authorizePageRequest = authorizePageBuilder.build();
response = client.newCall(authorizePageRequest).execute();
parseAuthorizeRequestData(response, data);
if (TextUtils.isEmpty(data.authenticityToken)) {
throw new AuthenticationException();
}
return data;
} catch (AttoParseException e) {
throw new AuthenticationException("Malformed HTML", e);
} finally {
Utils.closeSilently(response);
}
}
public static void readOAuthPINFromHtml(Reader reader, final OAuthPinData data) throws AttoParseException, IOException {
final HtmlParsingConfiguration conf = new HtmlParsingConfiguration();
final IAttoHandler handler = new AbstractStandardNonValidatingHtmlAttoHandler(conf) {
boolean isOAuthPinDivOpened;
@Override
public void handleHtmlStandaloneElement(IHtmlElement element, boolean minimized,
String elementName, Map<String, String> attributes,
int line, int col) {
handleHtmlOpenElement(element, elementName, attributes, line, col);
handleHtmlCloseElement(element, elementName, line, col);
}
@Override
public void handleHtmlOpenElement(IHtmlElement element, String elementName, Map<String, String> attributes, int line, int col) {
switch (elementName) {
case "div": {
if (attributes != null && "oauth_pin".equals(attributes.get("id"))) {
isOAuthPinDivOpened = true;
}
break;
}
}
}
@Override
public void handleHtmlCloseElement(IHtmlElement element, String elementName, int line, int col) {
if ("div".equals(elementName)) {
isOAuthPinDivOpened = false;
}
}
@Override
public void handleText(char[] buffer, int offset, int len, int line, int col) {
if (isOAuthPinDivOpened) {
final String s = new String(buffer, offset, len);
if (TextUtils.isDigitsOnly(s)) {
data.oauthPin = s;
}
}
}
};
PARSER.parse(reader, handler);
}
public interface LoginVerificationCallback {
String getLoginVerification(String challengeType);
}
public static class AuthenticationException extends Exception {
public AuthenticationException() {
}
public AuthenticationException(final Exception cause) {
super(cause);
}
public AuthenticationException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
public AuthenticationException(final String message) {
super(message);
}
}
public static final class AuthenticityTokenException extends AuthenticationException {
public AuthenticityTokenException(Exception e) {
super(e);
}
}
public static final class WrongUserPassException extends AuthenticationException {
WrongUserPassException() {
super();
}
WrongUserPassException(Exception cause) {
super(cause);
}
WrongUserPassException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
WrongUserPassException(String message) {
super(message);
}
}
public static final class LoginVerificationException extends AuthenticationException {
LoginVerificationException(String message) {
super(message);
}
LoginVerificationException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
LoginVerificationException(Exception cause) {
super(cause);
}
LoginVerificationException() {
super();
}
}
static class AuthorizeResponseData {
String referer;
public String oauthPin;
public Verification challenge;
static class Verification {
String authenticityToken;
String challengeId;
String challengeType;
String platform;
String userId;
String redirectAfterLogin;
}
}
static class AuthorizeRequestData {
public String authenticityToken;
public String redirectAfterLogin;
public String referer;
}
public static class OAuthPinData {
public String oauthPin;
}
private static class EndpointInterceptor implements Interceptor {
private final Endpoint endpoint;
public EndpointInterceptor(Endpoint endpoint) {
this.endpoint = endpoint;
}
@Override
public Response intercept(Chain chain) throws IOException {
final Response response = chain.proceed(chain.request());
if (!response.isRedirect()) {
return response;
}
final String location = response.header("Location");
final Response.Builder builder = response.newBuilder();
if (!TextUtils.isEmpty(location) && !endpoint.checkEndpoint(location)) {
final HttpUrl originalLocation = HttpUrl.get(URI.create("https://api.twitter.com/").resolve(location));
final HttpUrl.Builder locationBuilder = HttpUrl.parse(endpoint.getUrl()).newBuilder();
for (String pathSegments : originalLocation.pathSegments()) {
locationBuilder.addPathSegment(pathSegments);
}
for (int i = 0, j = originalLocation.querySize(); i < j; i++) {
final String name = originalLocation.queryParameterName(i);
final String value = originalLocation.queryParameterValue(i);
locationBuilder.addQueryParameter(name, value);
}
final String encodedFragment = originalLocation.encodedFragment();
if (encodedFragment != null) {
locationBuilder.encodedFragment(encodedFragment);
}
final HttpUrl newLocation = locationBuilder.build();
builder.header("Location", newLocation.toString());
}
return builder.build();
}
}
}

View File

@ -542,7 +542,7 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher {
class InputLoginVerificationDialogFragment : BaseDialogFragment(), DialogInterface.OnClickListener, DialogInterface.OnShowListener {
private var callback: SignInTask.InputLoginVerificationCallback? = null
private var challengeType: String? = null
var challengeType: String? = null
internal fun setCallback(callback: SignInTask.InputLoginVerificationCallback) {
this.callback = callback
@ -577,34 +577,36 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher {
}
}
fun setChallengeType(challengeType: String) {
this.challengeType = challengeType
}
override fun onShow(dialog: DialogInterface) {
val alertDialog = dialog as AlertDialog
val verificationHint = alertDialog.findViewById(R.id.verification_hint) as TextView?
val editVerification = alertDialog.findViewById(R.id.edit_verification_code) as EditText?
if (verificationHint == null || editVerification == null) return
if ("Push".equals(challengeType!!, ignoreCase = true)) {
verificationHint.setText(R.string.login_verification_push_hint)
editVerification.visibility = View.GONE
} else if ("RetypePhoneNumber".equals(challengeType!!, ignoreCase = true)) {
verificationHint.setText(R.string.login_challenge_retype_phone_hint)
editVerification.inputType = InputType.TYPE_CLASS_PHONE
editVerification.visibility = View.VISIBLE
} else if ("RetypeEmail".equals(challengeType!!, ignoreCase = true)) {
verificationHint.setText(R.string.login_challenge_retype_email_hint)
editVerification.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
editVerification.visibility = View.VISIBLE
} else if ("Sms".equals(challengeType!!, ignoreCase = true)) {
verificationHint.setText(R.string.login_verification_pin_hint)
editVerification.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
editVerification.visibility = View.VISIBLE
} else {
verificationHint.text = getString(R.string.unsupported_login_verification_type_name,
challengeType)
editVerification.visibility = View.VISIBLE
when {
"Push".equals(challengeType, ignoreCase = true) -> {
verificationHint.setText(R.string.login_verification_push_hint)
editVerification.visibility = View.GONE
}
"RetypePhoneNumber".equals(challengeType, ignoreCase = true) -> {
verificationHint.setText(R.string.login_challenge_retype_phone_hint)
editVerification.inputType = InputType.TYPE_CLASS_PHONE
editVerification.visibility = View.VISIBLE
}
"RetypeEmail".equals(challengeType, ignoreCase = true) -> {
verificationHint.setText(R.string.login_challenge_retype_email_hint)
editVerification.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
editVerification.visibility = View.VISIBLE
}
"Sms".equals(challengeType, ignoreCase = true) -> {
verificationHint.setText(R.string.login_verification_pin_hint)
editVerification.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
editVerification.visibility = View.VISIBLE
}
else -> {
verificationHint.text = getString(R.string.unsupported_login_verification_type_name,
challengeType)
editVerification.visibility = View.VISIBLE
}
}
}
}
@ -933,7 +935,7 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher {
field = value
}
override fun getLoginVerification(challengeType: String): String? {
override fun getLoginVerification(challengeType: String?): String? {
// Dismiss current progress dialog
publishProgress(Runnable {
val activity = activityRef.get() ?: return@Runnable
@ -947,7 +949,7 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher {
val df = InputLoginVerificationDialogFragment()
df.isCancelable = false
df.setCallback(this@InputLoginVerificationCallback)
df.setChallengeType(challengeType)
df.challengeType = challengeType
df.show(sia.supportFragmentManager, null)
Unit
}

View File

@ -0,0 +1,504 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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 org.mariotaku.twidere.util
import android.text.TextUtils
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Response
import org.attoparser.AttoParseException
import org.attoparser.markup.MarkupAttoParser
import org.attoparser.markup.html.AbstractStandardNonValidatingHtmlAttoHandler
import org.attoparser.markup.html.HtmlParsingConfiguration
import org.attoparser.markup.html.elements.IHtmlElement
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.TwitterOAuth
import org.mariotaku.restfu.RestAPIFactory
import org.mariotaku.restfu.annotation.method.GET
import org.mariotaku.restfu.annotation.method.POST
import org.mariotaku.restfu.http.*
import org.mariotaku.restfu.http.mime.FormBody
import org.mariotaku.restfu.http.mime.SimpleBody
import org.mariotaku.restfu.oauth.OAuthToken
import org.mariotaku.restfu.okhttp3.OkHttpRestClient
import org.mariotaku.twidere.TwidereConstants.OAUTH_CALLBACK_OOB
import org.mariotaku.twidere.util.net.SimpleCookieJar
import java.io.IOException
import java.io.Reader
import java.net.URI
class OAuthPasswordAuthenticator(
private val oauth: TwitterOAuth,
private val loginVerificationCallback: OAuthPasswordAuthenticator.LoginVerificationCallback,
private val userAgent: String
) {
private val client: RestHttpClient
private val endpoint: Endpoint
init {
val restClient = RestAPIFactory.getRestClient(oauth)
this.endpoint = restClient.endpoint
val oldClient = (restClient.restClient as OkHttpRestClient).client
val builder = oldClient.newBuilder()
builder.cookieJar(SimpleCookieJar())
builder.addNetworkInterceptor(EndpointInterceptor(endpoint))
this.client = OkHttpRestClient(builder.build())
}
@Throws(AuthenticationException::class)
fun getOAuthAccessToken(username: String, password: String): OAuthToken {
val requestToken: OAuthToken
try {
requestToken = oauth.getRequestToken(OAUTH_CALLBACK_OOB)
} catch (e: MicroBlogException) {
if (e.isCausedByNetworkIssue) throw AuthenticationException(e)
throw AuthenticityTokenException(e)
}
try {
val authorizeRequestData = getAuthorizeRequestData(requestToken)
var authorizeResponseData = getAuthorizeResponseData(requestToken,
authorizeRequestData, username, password)
if (!TextUtils.isEmpty(authorizeResponseData.oauthPin)) {
// Here we got OAuth PIN, just get access token directly
return oauth.getAccessToken(requestToken, authorizeResponseData.oauthPin)
} else if (authorizeResponseData.challenge == null) {
// No OAuth pin, or verification challenge, so treat as wrong password
throw WrongUserPassException()
}
// Go to password verification flow
val challengeType = authorizeResponseData.challenge!!.challengeType ?:
throw LoginVerificationException()
val loginVerification = loginVerificationCallback.getLoginVerification(challengeType)
val verificationData = getVerificationData(authorizeResponseData,
loginVerification)
authorizeResponseData = getAuthorizeResponseData(requestToken,
verificationData, username, password)
if (TextUtils.isEmpty(authorizeResponseData.oauthPin)) {
throw LoginVerificationException()
}
return oauth.getAccessToken(requestToken, authorizeResponseData.oauthPin)
} catch (e: IOException) {
throw AuthenticationException(e)
} catch (e: NullPointerException) {
throw AuthenticationException(e)
} catch (e: MicroBlogException) {
throw AuthenticationException(e)
}
}
@Throws(IOException::class, LoginVerificationException::class)
private fun getVerificationData(authorizeResponseData: AuthorizeResponseData,
challengeResponse: String?): AuthorizeRequestData {
var response: HttpResponse? = null
try {
val data = AuthorizeRequestData()
val params = MultiValueMap<String>()
val verification = authorizeResponseData.challenge!!
params.add("authenticity_token", verification.authenticityToken)
params.add("user_id", verification.userId)
params.add("challenge_id", verification.challengeId)
params.add("challenge_type", verification.challengeType)
params.add("platform", verification.platform)
params.add("redirect_after_login", verification.redirectAfterLogin)
val requestHeaders = MultiValueMap<String>()
requestHeaders.add("User-Agent", userAgent)
if (!TextUtils.isEmpty(challengeResponse)) {
params.add("challenge_response", challengeResponse)
}
val authorizationResultBody = FormBody(params)
val authorizeResultBuilder = HttpRequest.Builder()
authorizeResultBuilder.method(POST.METHOD)
authorizeResultBuilder.url(endpoint.construct("/account/login_verification"))
authorizeResultBuilder.headers(requestHeaders)
authorizeResultBuilder.body(authorizationResultBody)
response = client.newCall(authorizeResultBuilder.build()).execute()
parseAuthorizeRequestData(response, data)
if (TextUtils.isEmpty(data.authenticityToken)) {
throw LoginVerificationException()
}
return data
} catch (e: AttoParseException) {
throw LoginVerificationException("Login verification challenge failed", e)
} finally {
Utils.closeSilently(response)
}
}
@Throws(AttoParseException::class, IOException::class)
private fun parseAuthorizeRequestData(response: HttpResponse, data: AuthorizeRequestData) {
val conf = HtmlParsingConfiguration()
val handler = object : AbstractStandardNonValidatingHtmlAttoHandler(conf) {
internal var isOAuthFormOpened: Boolean = false
override fun handleHtmlStandaloneElement(element: IHtmlElement?, minimized: Boolean,
elementName: String?, attributes: Map<String, String>?,
line: Int, col: Int) {
handleHtmlOpenElement(element, elementName, attributes, line, col)
handleHtmlCloseElement(element, elementName, line, col)
}
override fun handleHtmlOpenElement(element: IHtmlElement?, elementName: String?,
attributes: Map<String, String>?, line: Int, col: Int) {
when (elementName) {
"form" -> {
if (attributes != null && "oauth_form" == attributes["id"]) {
isOAuthFormOpened = true
}
}
"input" -> {
if (attributes != null && isOAuthFormOpened) {
val name = attributes["name"]
val value = attributes["value"]
if (name == "authenticity_token") {
data.authenticityToken = value
} else if (name == "redirect_after_login") {
data.redirectAfterLogin = value
}
}
}
}
}
override fun handleHtmlCloseElement(element: IHtmlElement?, elementName: String?, line: Int, col: Int) {
if ("form" == elementName) {
isOAuthFormOpened = false
}
}
}
PARSER.parse(SimpleBody.reader(response.body), handler)
}
@Throws(IOException::class, AuthenticationException::class)
private fun getAuthorizeResponseData(requestToken: OAuthToken,
authorizeRequestData: AuthorizeRequestData,
username: String, password: String): AuthorizeResponseData {
var response: HttpResponse? = null
try {
val data = AuthorizeResponseData()
val params = MultiValueMap<String>()
params.add("oauth_token", requestToken.oauthToken)
params.add("authenticity_token", authorizeRequestData.authenticityToken)
params.add("redirect_after_login", authorizeRequestData.redirectAfterLogin)
if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) {
params.add("session[username_or_email]", username)
params.add("session[password]", password)
}
val authorizationResultBody = FormBody(params)
val requestHeaders = MultiValueMap<String>()
requestHeaders.add("User-Agent", userAgent)
data.referer = authorizeRequestData.referer
val authorizeResultBuilder = HttpRequest.Builder()
authorizeResultBuilder.method(POST.METHOD)
authorizeResultBuilder.url(endpoint.construct("/oauth/authorize"))
authorizeResultBuilder.headers(requestHeaders)
authorizeResultBuilder.body(authorizationResultBody)
response = client.newCall(authorizeResultBuilder.build()).execute()
val conf = HtmlParsingConfiguration()
val handler = object : AbstractStandardNonValidatingHtmlAttoHandler(conf) {
internal var isOAuthPinDivOpened: Boolean = false
internal var isChallengeFormOpened: Boolean = false
override fun handleHtmlStandaloneElement(element: IHtmlElement?, minimized: Boolean,
elementName: String?, attributes: Map<String, String>?,
line: Int, col: Int) {
handleHtmlOpenElement(element, elementName, attributes, line, col)
handleHtmlCloseElement(element, elementName, line, col)
}
override fun handleHtmlCloseElement(element: IHtmlElement?, elementName: String?, line: Int, col: Int) {
when (elementName) {
"div" -> {
isOAuthPinDivOpened = false
}
"form" -> {
isChallengeFormOpened = false
}
}
}
override fun handleHtmlOpenElement(element: IHtmlElement?, elementName: String?,
attributes: Map<String, String>?, line: Int, col: Int) {
when (elementName) {
"div" -> {
if (attributes != null && "oauth_pin" == attributes["id"]) {
isOAuthPinDivOpened = true
}
}
"form" -> {
if (attributes != null) when (attributes["id"]) {
"login-verification-form", "login-challenge-form" -> {
isChallengeFormOpened = true
}
}
}
"input" -> {
if (attributes != null && isChallengeFormOpened) {
val name = attributes["name"]
val value = attributes["value"]
when (name) {
"authenticity_token" -> {
ensureVerification()
data.challenge!!.authenticityToken = value
}
"challenge_id" -> {
ensureVerification()
data.challenge!!.challengeId = value
}
"challenge_type" -> {
ensureVerification()
data.challenge!!.challengeType = value
}
"platform" -> {
ensureVerification()
data.challenge!!.platform = value
}
"user_id" -> {
ensureVerification()
data.challenge!!.userId = value
}
"redirect_after_login" -> {
ensureVerification()
data.challenge!!.redirectAfterLogin = value
}
}
}
}
}
}
private fun ensureVerification() {
if (data.challenge == null) {
data.challenge = AuthorizeResponseData.Verification()
}
}
@Throws(AttoParseException::class)
override fun handleText(buffer: CharArray?, offset: Int, len: Int, line: Int, col: Int) {
if (isOAuthPinDivOpened) {
val s = String(buffer!!, offset, len)
if (TextUtils.isDigitsOnly(s)) {
data.oauthPin = s
}
}
}
}
PARSER.parse(SimpleBody.reader(response!!.body), handler)
return data
} catch (e: AttoParseException) {
throw AuthenticationException("Malformed HTML", e)
} finally {
Utils.closeSilently(response)
}
}
@Throws(IOException::class, AuthenticationException::class)
private fun getAuthorizeRequestData(requestToken: OAuthToken): AuthorizeRequestData {
var response: HttpResponse? = null
try {
val data = AuthorizeRequestData()
val authorizePageBuilder = HttpRequest.Builder()
authorizePageBuilder.method(GET.METHOD)
authorizePageBuilder.url(endpoint.construct("/oauth/authorize",
arrayOf("oauth_token", requestToken.oauthToken)))
data.referer = Endpoint.constructUrl("https://api.twitter.com/oauth/authorize",
arrayOf("oauth_token", requestToken.oauthToken))
val requestHeaders = MultiValueMap<String>()
requestHeaders.add("User-Agent", userAgent)
authorizePageBuilder.headers(requestHeaders)
val authorizePageRequest = authorizePageBuilder.build()
response = client.newCall(authorizePageRequest).execute()
parseAuthorizeRequestData(response, data)
if (TextUtils.isEmpty(data.authenticityToken)) {
throw AuthenticationException()
}
return data
} catch (e: AttoParseException) {
throw AuthenticationException("Malformed HTML", e)
} finally {
Utils.closeSilently(response)
}
}
interface LoginVerificationCallback {
fun getLoginVerification(challengeType: String): String?
}
open class AuthenticationException : Exception {
constructor() {
}
constructor(cause: Exception) : super(cause) {
}
constructor(detailMessage: String, throwable: Throwable) : super(detailMessage, throwable) {
}
constructor(message: String) : super(message) {
}
}
class AuthenticityTokenException(e: Exception) : AuthenticationException(e)
class WrongUserPassException : AuthenticationException {
internal constructor() : super() {
}
internal constructor(cause: Exception) : super(cause) {
}
internal constructor(detailMessage: String, throwable: Throwable) : super(detailMessage, throwable) {
}
internal constructor(message: String) : super(message) {
}
}
class LoginVerificationException : AuthenticationException {
internal constructor(message: String) : super(message) {
}
internal constructor(detailMessage: String, throwable: Throwable) : super(detailMessage, throwable) {
}
internal constructor(cause: Exception) : super(cause) {
}
internal constructor() : super() {
}
}
internal class AuthorizeResponseData {
var referer: String? = null
var oauthPin: String? = null
var challenge: Verification? = null
internal class Verification {
var authenticityToken: String? = null
var challengeId: String? = null
var challengeType: String? = null
var platform: String? = null
var userId: String? = null
var redirectAfterLogin: String? = null
}
}
internal class AuthorizeRequestData {
var authenticityToken: String? = null
var redirectAfterLogin: String? = null
var referer: String? = null
}
class OAuthPinData {
var oauthPin: String? = null
}
private class EndpointInterceptor(private val endpoint: Endpoint) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (!response.isRedirect) {
return response
}
val location = response.header("Location")
val builder = response.newBuilder()
if (!TextUtils.isEmpty(location) && !endpoint.checkEndpoint(location)) {
val originalLocation = HttpUrl.get(URI.create("https://api.twitter.com/").resolve(location))
val locationBuilder = HttpUrl.parse(endpoint.url).newBuilder()
for (pathSegments in originalLocation.pathSegments()) {
locationBuilder.addPathSegment(pathSegments)
}
var i = 0
val j = originalLocation.querySize()
while (i < j) {
val name = originalLocation.queryParameterName(i)
val value = originalLocation.queryParameterValue(i)
locationBuilder.addQueryParameter(name, value)
i++
}
val encodedFragment = originalLocation.encodedFragment()
if (encodedFragment != null) {
locationBuilder.encodedFragment(encodedFragment)
}
val newLocation = locationBuilder.build()
builder.header("Location", newLocation.toString())
}
return builder.build()
}
}
companion object {
private val PARSER = MarkupAttoParser()
@Throws(AttoParseException::class, IOException::class)
fun readOAuthPINFromHtml(reader: Reader, data: OAuthPinData) {
val conf = HtmlParsingConfiguration()
val handler = object : AbstractStandardNonValidatingHtmlAttoHandler(conf) {
internal var isOAuthPinDivOpened: Boolean = false
override fun handleHtmlStandaloneElement(element: IHtmlElement?, minimized: Boolean,
elementName: String?, attributes: Map<String, String>?,
line: Int, col: Int) {
handleHtmlOpenElement(element, elementName, attributes, line, col)
handleHtmlCloseElement(element, elementName, line, col)
}
override fun handleHtmlOpenElement(element: IHtmlElement?, elementName: String?, attributes: Map<String, String>?, line: Int, col: Int) {
when (elementName) {
"div" -> {
if (attributes != null && "oauth_pin" == attributes["id"]) {
isOAuthPinDivOpened = true
}
}
}
}
override fun handleHtmlCloseElement(element: IHtmlElement?, elementName: String?, line: Int, col: Int) {
if ("div" == elementName) {
isOAuthPinDivOpened = false
}
}
override fun handleText(buffer: CharArray?, offset: Int, len: Int, line: Int, col: Int) {
if (isOAuthPinDivOpened) {
val s = String(buffer!!, offset, len)
if (TextUtils.isDigitsOnly(s)) {
data.oauthPin = s
}
}
}
}
PARSER.parse(reader, handler)
}
}
}