fixed crashes
This commit is contained in:
parent
dc8d7bd891
commit
9593365fa5
|
@ -351,7 +351,7 @@ public class MicroBlogAPIFactory implements TwidereConstants {
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
static String substituteLegacyApiBaseUrl(@NonNull String format, String domain) {
|
static String substituteLegacyApiBaseUrl(@NonNull String format, @Nullable String domain) {
|
||||||
final int idxOfSlash = format.indexOf("://");
|
final int idxOfSlash = format.indexOf("://");
|
||||||
// Not an url
|
// Not an url
|
||||||
if (idxOfSlash < 0) return format;
|
if (idxOfSlash < 0) return format;
|
||||||
|
@ -362,8 +362,12 @@ public class MicroBlogAPIFactory implements TwidereConstants {
|
||||||
if (!host.equalsIgnoreCase("api.twitter.com")) return format;
|
if (!host.equalsIgnoreCase("api.twitter.com")) return format;
|
||||||
final StringBuilder sb = new StringBuilder();
|
final StringBuilder sb = new StringBuilder();
|
||||||
sb.append(format.substring(0, startOfHost));
|
sb.append(format.substring(0, startOfHost));
|
||||||
sb.append(domain);
|
if (domain != null) {
|
||||||
sb.append(".twitter.com");
|
sb.append(domain);
|
||||||
|
sb.append(".twitter.com");
|
||||||
|
} else {
|
||||||
|
sb.append("twitter.com");
|
||||||
|
}
|
||||||
if (endOfHost != -1) {
|
if (endOfHost != -1) {
|
||||||
sb.append(format.substring(endOfHost));
|
sb.append(format.substring(endOfHost));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -542,7 +542,7 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher {
|
||||||
class InputLoginVerificationDialogFragment : BaseDialogFragment(), DialogInterface.OnClickListener, DialogInterface.OnShowListener {
|
class InputLoginVerificationDialogFragment : BaseDialogFragment(), DialogInterface.OnClickListener, DialogInterface.OnShowListener {
|
||||||
|
|
||||||
private var callback: SignInTask.InputLoginVerificationCallback? = null
|
private var callback: SignInTask.InputLoginVerificationCallback? = null
|
||||||
private var challengeType: String? = null
|
var challengeType: String? = null
|
||||||
|
|
||||||
internal fun setCallback(callback: SignInTask.InputLoginVerificationCallback) {
|
internal fun setCallback(callback: SignInTask.InputLoginVerificationCallback) {
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
|
@ -577,34 +577,36 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setChallengeType(challengeType: String) {
|
|
||||||
this.challengeType = challengeType
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onShow(dialog: DialogInterface) {
|
override fun onShow(dialog: DialogInterface) {
|
||||||
val alertDialog = dialog as AlertDialog
|
val alertDialog = dialog as AlertDialog
|
||||||
val verificationHint = alertDialog.findViewById(R.id.verification_hint) as TextView?
|
val verificationHint = alertDialog.findViewById(R.id.verification_hint) as TextView?
|
||||||
val editVerification = alertDialog.findViewById(R.id.edit_verification_code) as EditText?
|
val editVerification = alertDialog.findViewById(R.id.edit_verification_code) as EditText?
|
||||||
if (verificationHint == null || editVerification == null) return
|
if (verificationHint == null || editVerification == null) return
|
||||||
if ("Push".equals(challengeType!!, ignoreCase = true)) {
|
when {
|
||||||
verificationHint.setText(R.string.login_verification_push_hint)
|
"Push".equals(challengeType, ignoreCase = true) -> {
|
||||||
editVerification.visibility = View.GONE
|
verificationHint.setText(R.string.login_verification_push_hint)
|
||||||
} else if ("RetypePhoneNumber".equals(challengeType!!, ignoreCase = true)) {
|
editVerification.visibility = View.GONE
|
||||||
verificationHint.setText(R.string.login_challenge_retype_phone_hint)
|
}
|
||||||
editVerification.inputType = InputType.TYPE_CLASS_PHONE
|
"RetypePhoneNumber".equals(challengeType, ignoreCase = true) -> {
|
||||||
editVerification.visibility = View.VISIBLE
|
verificationHint.setText(R.string.login_challenge_retype_phone_hint)
|
||||||
} else if ("RetypeEmail".equals(challengeType!!, ignoreCase = true)) {
|
editVerification.inputType = InputType.TYPE_CLASS_PHONE
|
||||||
verificationHint.setText(R.string.login_challenge_retype_email_hint)
|
editVerification.visibility = View.VISIBLE
|
||||||
editVerification.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
}
|
||||||
editVerification.visibility = View.VISIBLE
|
"RetypeEmail".equals(challengeType, ignoreCase = true) -> {
|
||||||
} else if ("Sms".equals(challengeType!!, ignoreCase = true)) {
|
verificationHint.setText(R.string.login_challenge_retype_email_hint)
|
||||||
verificationHint.setText(R.string.login_verification_pin_hint)
|
editVerification.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
||||||
editVerification.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
editVerification.visibility = View.VISIBLE
|
||||||
editVerification.visibility = View.VISIBLE
|
}
|
||||||
} else {
|
"Sms".equals(challengeType, ignoreCase = true) -> {
|
||||||
verificationHint.text = getString(R.string.unsupported_login_verification_type_name,
|
verificationHint.setText(R.string.login_verification_pin_hint)
|
||||||
challengeType)
|
editVerification.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||||
editVerification.visibility = View.VISIBLE
|
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
|
field = value
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLoginVerification(challengeType: String): String? {
|
override fun getLoginVerification(challengeType: String?): String? {
|
||||||
// Dismiss current progress dialog
|
// Dismiss current progress dialog
|
||||||
publishProgress(Runnable {
|
publishProgress(Runnable {
|
||||||
val activity = activityRef.get() ?: return@Runnable
|
val activity = activityRef.get() ?: return@Runnable
|
||||||
|
@ -947,7 +949,7 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher {
|
||||||
val df = InputLoginVerificationDialogFragment()
|
val df = InputLoginVerificationDialogFragment()
|
||||||
df.isCancelable = false
|
df.isCancelable = false
|
||||||
df.setCallback(this@InputLoginVerificationCallback)
|
df.setCallback(this@InputLoginVerificationCallback)
|
||||||
df.setChallengeType(challengeType)
|
df.challengeType = challengeType
|
||||||
df.show(sia.supportFragmentManager, null)
|
df.show(sia.supportFragmentManager, null)
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue