All Downloads are FREE. Search and download functionalities are using the official Maven repository.

xyz.cssxsh.pixiv.OtherAuth.kt Maven / Gradle / Ivy

package xyz.cssxsh.pixiv

import io.ktor.client.call.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.cookies.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.*
import org.openqa.selenium.WebDriverException
import org.openqa.selenium.logging.*
import org.openqa.selenium.remote.*
import xyz.cssxsh.pixiv.auth.*
import xyz.cssxsh.pixiv.tool.*

public const val WEIBO_QRCODE_GENERATE: String = "https://api.weibo.com/oauth2/qrcode_authorize/generate"

public const val WEIBO_QRCODE_QUERY: String = "https://api.weibo.com/oauth2/qrcode_authorize/query"

public const val WEIBO_QRCODE_TIMEOUT: Long = 10 * 60 * 1000L

public const val WEIBO_QRCODE_INTERVAL: Long = 3 * 1000L

private fun HttpMessage.location() = headers[HttpHeaders.Location]?.let(::Url)

private fun account(html: String): HtmlAccount {
    val data = html.substringAfter("value='").substringBefore("'")
    return PixivJson.decodeFromString(HtmlAccount.serializer(), data)
}

/**
 * @param link for [POST_REDIRECT_URL]
 */
private suspend fun PixivAuthClient.redirect(link: Url): String {
    val redirect = useHttpClient { it.get(link) }
    val location = redirect.request.url

    val account = account(html = redirect.body())

    checkNotNull(account.uid) { "未登录" }

    val current = Url(location.parameters["return_to"]?.decodeURLPart() ?: account.returnTo)

    if (location.host == "accounts.pixiv.net") {
        val selected = useHttpClient { client ->
            client.submitForm(url = POST_SELECTED_URL, formParameters = Parameters.build {
                append("tt", account.token)
                append("return_to", URLBuilder(POST_REDIRECT_URL).apply {
                    parameters.append("return_to", current.toString())
                }.buildString())
            }) {
                expectSuccess = false

                header(HttpHeaders.Origin, ORIGIN_URL)
                header(HttpHeaders.Referrer, location)
            }
        }
        selected.location()?.let { url ->
            val message = useHttpClient { client ->
                client.get(url) {

                    header(HttpHeaders.Origin, ORIGIN_URL)
                    header(HttpHeaders.Referrer, location)
                }
            }
            message.bodyAsText()
        }
    }

    /**
     * current for [START_URL]
     */
    val response = useHttpClient { client ->
        client.post(current) {
            expectSuccess = false

            parameter("via", "login")

            header(HttpHeaders.Origin, ORIGIN_URL)
            header(HttpHeaders.Referrer, LOGIN_URL)
        }
    }

    val authorize = requireNotNull(response.location()) { "跳转到 $OAUTH_AUTHORIZE_URL 失败" }

    /**
     * authorize for [OAUTH_AUTHORIZE_URL]
     */
    val code = useHttpClient { client ->
        client.get(authorize) {
            expectSuccess = false

            header(HttpHeaders.Origin, ORIGIN_URL)
            header(HttpHeaders.Referrer, LOGIN_URL)
        }
    }

    code.contentType()?.let { type ->
        java.io.File("pixiv.login.error.${type.contentSubtype}")
            .writeBytes(code.body())
        java.io.File("pixiv.login.cookies")
            .writeText(storage.get(authorize).joinToString("\n", transform = ::renderCookieHeader))
    }

    val scheme = requireNotNull(code.location()) { "跳转到 pixiv://... 失败" }

    /**
     * Code by Url pixiv://...
     */
    return scheme.parameters["code"] ?: throw NoSuchElementException("code, $scheme")
}

/**
 * 登录,通过新浪微博关联Pixiv
 * @param show handle qrcode image url
 */
public suspend fun PixivAuthClient.sina(show: suspend (qrcode: Url) -> Unit): AuthResult = login { redirect ->
    /**
     * for [LOGIN_URL]
     */
    val account = account(html = useHttpClient { it.get(redirect).body() })

    /**
     * for [GIGYA_AUTH_URL]
     */
    val response = useHttpClient { client ->
        client.submitForm(url = GIGYA_AUTH_URL, formParameters = Parameters.build {
            append("tt", account.token)
            append("mode", "signin")
            append("provider", "sina")
            append("lang", "zh")
            append("ref_sns_button", "login")
            append("return_to", redirect.toString())
            append("source", "pixiv-android")
            append("view_type", "page")
        }) {
            expectSuccess = false

            header(HttpHeaders.Origin, ORIGIN_URL)
            header(HttpHeaders.Referrer, LOGIN_URL)
        }
    }

    val socialize = requireNotNull(response.location()) { "跳转到 $SOCIALIZE_LOGIN_URL 失败" }

    // Jump to Sina Weibo
    val temp = useHttpClient { it.head(socialize) }
    // 为了直接继承参数 ↓
    val qrcode = PixivJson.decodeFromString(WeiboQrcode.serializer(), useHttpClient { client ->
        client.get {
            takeFrom(temp.request)
            url {
                encodedPath = Url(WEIBO_QRCODE_GENERATE).encodedPath
            }
        }.body()
    })

    supervisorScope {
        show(Url(qrcode.url))
    }

    val jump: String = withTimeout(WEIBO_QRCODE_TIMEOUT) {
        while (isActive) {
            delay(WEIBO_QRCODE_INTERVAL)
            val status = PixivJson.decodeFromString(WeiboQrcodeStatus.serializer(), useHttpClient { client ->
                client.get(WEIBO_QRCODE_QUERY) {
                    parameter("vcode", qrcode.vcode)
                }.body()
            })
            return@withTimeout status.url ?: continue
        }
        throw CancellationException("WEIBO_QRCODE_TIMEOUT")
    }

    // replace protocol for ssl with g-client-proxy.net
    val gigya: String = useHttpClient { client ->
        client.get(jump) {
            url {
                protocol = URLProtocol.HTTPS
                host = "d1ctzrip8l97jt.cloudfront.net"
            }
        }.body()
    }

    val sign = gigya.substringAfter("redirect('").substringBefore("');")
    // transform unicode
    val unicode = """\\u(\d{4})""".toRegex()
    val link = Url(sign.replace(unicode) { it.groupValues[1].toInt(16).toChar().toString() })

    /**
     * GiGya auto to [POST_REDIRECT_URL]
     */
    redirect(link = link)
}

/**
 * 登录,通过 Web Cookies
 * @param load get pixiv web cookie
 */
public suspend fun PixivAuthClient.cookie(load: () -> List): AuthResult = login { redirect ->
    val origin = Url(ORIGIN_URL)
    for (cookie in load()) {
        storage.addCookie(origin, cookie)
    }
    requireNotNull(storage.get(origin)["PHPSESSID"]) { "PHPSESSID is null" }
    val account = account(html = useHttpClient { it.get(redirect).body() })

    checkNotNull(account.uid) { "未登录" }

    /**
     * for [POST_SELECTED_URL]
     */
    val response = useHttpClient { client ->
        client.submitForm(url = POST_SELECTED_URL, formParameters = Parameters.build {
            append("return_to", account.current.ifEmpty { redirect.toString() })
            append("tt", account.token)
        }) {
            expectSuccess = false

            header(HttpHeaders.Origin, ORIGIN_URL)
            header(HttpHeaders.Referrer, LOGIN_URL)
        }
    }

    val link = requireNotNull(response.location()) { "跳转到 $POST_REDIRECT_URL 失败" }

    redirect(link = link)
}

/**
 * 登录,通过 账户密码
 * @param id Pixiv ID 或者 Email
 * @param pwd 密码
 * @param handler 人机验证处理
 */
public suspend fun PixivAuthClient.password(id: String, pwd: String, handler: CaptchaHandler): AuthResult = login { r ->
    /**
     * for [LOGIN_URL]
     */
    val login = useHttpClient { it.get(r) }
    val account = account(html = login.body())

    while (isActive) {
        /**
         * 实际上 SiteKey 是固定值 6LfF1dcZAAAAAOHQX8v16MX5SktDwmQINVD_6mBF
         */
        val gRecaptchaResponse =
            handler.handle(siteKey = account.scoreSiteKey, referer = login.request.url.toString())

        val result: WebLoginResult = useHttpClient { client ->
            client.submitForm(url = LOGIN_API_URL, formParameters = Parameters.build {
                append("password", pwd)
                append("pixiv_id", id)
                append("captcha", "")
                append("g-recaptcha-response", gRecaptchaResponse)
                append("post_key", account.postKey)
                append("source", account.source)
                append("ref", account.ref)
                append("return_to", account.returnTo)
                append("recaptcha_enterprise_score_token", gRecaptchaResponse)
                append("tt", account.token)
            }) {
                header(HttpHeaders.Origin, ORIGIN_URL)
                header(HttpHeaders.Referrer, "https://accounts.pixiv.net/")

                parameter("lang", "zh")
            }.body()
        }

        if ("captcha" in result.body.validationErrors) continue

        check(result.body.validationErrors.isNotEmpty()) { result.body }

        break
    }

    /**
     * for [POST_SELECTED_URL]
     */
    val message = useHttpClient { client ->
        client.submitForm(url = POST_SELECTED_URL, formParameters = Parameters.build {
            append("return_to", account.current)
            append("tt", account.token)
        }) {
            expectSuccess = false

            header(HttpHeaders.Origin, ORIGIN_URL)
            header(HttpHeaders.Referrer, LOGIN_URL)
        }
    }

    val link = requireNotNull(message.location()) { "跳转到 $POST_REDIRECT_URL 失败" }

    redirect(link = link)
}

/**
 * 登录,通过 selenium 调用浏览器
 * @param driver 浏览器驱动器
 */
public suspend fun PixivAuthClient.selenium(driver: RemoteWebDriver, timeout: Long): AuthResult = login { redirect ->
    try {
        driver.get(redirect.toString())
    } catch (_: WebDriverException) {
        //
    }
    withTimeout(timeout) {
        while (driver.currentUrl.orEmpty().startsWith(POST_REDIRECT_URL).not()) {
            delay(10_000)
        }
    }
    // XXX: 通过错误日志获取 跳转URL
    val log = driver.manage().logs().get(LogType.BROWSER)
        .findLast { log -> "pixiv://account/login" in log.message.orEmpty() }
        ?: throw NoSuchElementException("No Found pixiv://account/login")
    val url = Url(log.message.substringAfter("'").substringBefore("'"))

    url.parameters["code"] ?: throw NoSuchElementException("code, ${log.message}")
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy