474 lines
20 KiB
Kotlin
474 lines
20 KiB
Kotlin
/*
|
|
* 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.ParseException
|
|
import org.attoparser.config.ParseConfiguration
|
|
import org.attoparser.simple.AbstractSimpleMarkupHandler
|
|
import org.attoparser.simple.SimpleMarkupParser
|
|
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
|
|
|
|
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 {
|
|
try {
|
|
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)
|
|
return client.newCall(authorizeResultBuilder.build()).execute().use {
|
|
val data = AuthorizeRequestData()
|
|
parseAuthorizeRequestData(it, data)
|
|
if (data.authenticityToken.isNullOrEmpty()) {
|
|
throw LoginVerificationException()
|
|
}
|
|
return@use data
|
|
}
|
|
} catch (e: ParseException) {
|
|
throw LoginVerificationException("Login verification challenge failed", e)
|
|
}
|
|
}
|
|
|
|
@Throws(ParseException::class, IOException::class)
|
|
private fun parseAuthorizeRequestData(response: HttpResponse, data: AuthorizeRequestData) {
|
|
val handler = object : AbstractSimpleMarkupHandler() {
|
|
var isOAuthFormOpened: Boolean = false
|
|
override fun handleStandaloneElement(elementName: String, attributes: MutableMap<String, String>?, minimized: Boolean, line: Int, col: Int) {
|
|
handleOpenElement(elementName, attributes, line, col)
|
|
handleCloseElement(elementName, line, col)
|
|
}
|
|
|
|
override fun handleOpenElement(elementName: String, attributes: MutableMap<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 handleCloseElement(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 {
|
|
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)
|
|
val handler = object : AbstractSimpleMarkupHandler() {
|
|
var isOAuthPinDivOpened: Boolean = false
|
|
var isChallengeFormOpened: Boolean = false
|
|
|
|
override fun handleStandaloneElement(elementName: String, attributes: MutableMap<String, String>?,
|
|
minimized: Boolean, line: Int, col: Int) {
|
|
handleOpenElement(elementName, attributes, line, col)
|
|
handleCloseElement(elementName, line, col)
|
|
}
|
|
|
|
override fun handleCloseElement(elementName: String, line: Int, col: Int) {
|
|
when (elementName) {
|
|
"div" -> {
|
|
isOAuthPinDivOpened = false
|
|
}
|
|
"form" -> {
|
|
isChallengeFormOpened = false
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun handleOpenElement(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(ParseException::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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
client.newCall(authorizeResultBuilder.build()).execute().use {
|
|
PARSER.parse(SimpleBody.reader(it.body), handler)
|
|
}
|
|
return data
|
|
} catch (e: ParseException) {
|
|
throw AuthenticationException("Malformed HTML", e)
|
|
}
|
|
}
|
|
|
|
@Throws(IOException::class, AuthenticationException::class)
|
|
private fun getAuthorizeRequestData(requestToken: OAuthToken): AuthorizeRequestData {
|
|
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()
|
|
client.newCall(authorizePageRequest).execute().use {
|
|
parseAuthorizeRequestData(it, data)
|
|
}
|
|
if (data.authenticityToken.isNullOrEmpty()) {
|
|
throw AuthenticationException()
|
|
}
|
|
return data
|
|
} catch (e: ParseException) {
|
|
throw AuthenticationException("Malformed HTML", e)
|
|
}
|
|
}
|
|
|
|
interface LoginVerificationCallback {
|
|
@Throws(MicroBlogException::class)
|
|
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.parse("https://api.twitter.com/")?.resolve(location)!!
|
|
val locationBuilder = HttpUrl.parse(endpoint.url)!!.newBuilder()
|
|
for (pathSegments in originalLocation.pathSegments()) {
|
|
locationBuilder.addPathSegment(pathSegments)
|
|
}
|
|
for (i in 0 until originalLocation.querySize()) {
|
|
val name = originalLocation.queryParameterName(i)
|
|
val value = originalLocation.queryParameterValue(i)
|
|
locationBuilder.addQueryParameter(name, value)
|
|
}
|
|
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 = SimpleMarkupParser(ParseConfiguration.htmlConfiguration())
|
|
|
|
@Throws(ParseException::class, IOException::class)
|
|
fun readOAuthPINFromHtml(reader: Reader, data: OAuthPinData) {
|
|
val handler = object : AbstractSimpleMarkupHandler() {
|
|
var isOAuthPinDivOpened: Boolean = false
|
|
override fun handleStandaloneElement(elementName: String,
|
|
attributes: MutableMap<String, String>?,
|
|
minimized: Boolean, line: Int, col: Int) {
|
|
handleOpenElement(elementName, attributes, line, col)
|
|
handleCloseElement(elementName, line, col)
|
|
}
|
|
|
|
override fun handleOpenElement(elementName: String, attributes: Map<String, String>?, line: Int, col: Int) {
|
|
when (elementName) {
|
|
"div" -> {
|
|
if (attributes != null && "oauth_pin" == attributes["id"]) {
|
|
isOAuthPinDivOpened = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun handleCloseElement(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)
|
|
}
|
|
}
|
|
}
|