throw Exception when login fail

This commit is contained in:
czp3009
2019-07-09 16:58:03 +08:00
parent 1fd431013a
commit 759715b8ef
9 changed files with 93 additions and 23 deletions

View File

@@ -9,11 +9,45 @@ RestFul API
compile group: 'com.hiczp', name: 'bilibili-api-rest', version: '0.2.0'
```
Websocket(用于连接直播间弹幕推送服务器以获取实时弹幕)
Websocket(用于获取直播间实时弹幕)
```groovy
compile group: 'com.hiczp', name: 'bilibili-api-websocket', version: '0.2.0'
```
# RestFul API
大部分 API 都是 RestFul API.
`BilibiliClient` 是一个模拟的客户端, 内含登陆状态. 应持有其引用, 并在合适时执行 `close()`.
## 登陆
```kotlin
val bilibiliClient = BilibiliClient()
runBlocking {
bilibiliClient.login(username, password)
}
```
`BilibiliClient.login` 会返回一个 `Credential` 实例. 将其序列化后保存. 下次可以直接使用这一凭证来恢复登陆状态
```kotlin
val bilibiliClient = BilibiliClient(credential)
```
多次错误的登陆将导致下一次登陆需要验证码(极验).
登陆失败将抛出 `LoginException` 异常, 其中包含服务器原始返回内容 `LoginResponse`.
## 登出
```kotlin
runBlocking {
bilibiliClient.revoke()
}
```
`BilibiliClient.revoke` 返回已被注销的凭证, 或返回 `null` 当此 `BilibiliClient` 实例没有包含凭证时.
如果凭证是错误的, 将抛出 `RevokeException`.
# 获取直播间实时弹幕
举个例子
```kotlin
@@ -49,9 +83,10 @@ runBlocking {
```kotlin
val command = commandPacket.content
val danmakuMessage = command.asDanmakuMessage()
with(danmakuMessage) {
println("$nickname: $message")
if (command.cmd == "DANMU_MSG") {
with(command.asDanmakuMessage()) {
println("$nickname: $message")
}
}
```
@@ -59,7 +94,7 @@ with(danmakuMessage) {
如果要为读取操作设定超时, 可以设定为 40秒.
注意: 如果使用短房间号来连接弹幕推送服务器, 可能会得不到正确的人气值信息(一直为 0 或者一直为 1). 因此在连接弹幕推送服务器前应当首先获取直播间基本信息. 同时, 弹幕服务器不是唯一的, 在构造 `LiveClient` 时可以传入从房间基本信息中获取到的其他服务器地址.
注意: 如果使用短房间号来连接弹幕推送服务器, 可能会得不到正确的人气值信息(一直为 0 或者一直为 1). 因此在连接弹幕推送服务器前应当首先获取直播间基本信息. 同时, 弹幕服务器不是唯一的, 在构造 `LiveClient` 时可以传入其他服务器地址.
# License
GPL V3

View File

@@ -1,14 +1,9 @@
package com.hiczp.bilibili.rest
import com.hiczp.bilibili.rest.ktor.appendMissing
import com.hiczp.bilibili.rest.ktor.commonParams
import com.hiczp.bilibili.rest.ktor.logging
import com.hiczp.bilibili.rest.ktor.rsaEncrypt
import com.hiczp.bilibili.rest.ktor.*
import com.hiczp.bilibili.rest.service.live.LiveService
import com.hiczp.bilibili.rest.service.passport.Credential
import com.hiczp.bilibili.rest.service.passport.PassportService
import com.hiczp.bilibili.rest.service.passport.cookieMap
import com.hiczp.bilibili.rest.service.passport.toCredential
import com.hiczp.bilibili.rest.service.orThrow
import com.hiczp.bilibili.rest.service.passport.*
import com.hiczp.caeruleum.create
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
@@ -41,6 +36,7 @@ class BilibiliClient(credential: Credential? = null) : Closeable {
*
* @return new credential
*/
@Throws(LoginException::class)
suspend fun login(
username: String, password: String,
//极验
@@ -49,10 +45,12 @@ class BilibiliClient(credential: Credential? = null) : Closeable {
validate: String? = null
): Credential {
val (hash, key) = PassportService.getKey().let { response ->
response.hash to response.key.split('\n').filterNot { it.startsWith('-') }.joinToString(separator = "")
response.hash to response.key.toDER()
}
val cipheredPassword = (hash + password).rsaEncrypt(key)
return PassportService.login(username, cipheredPassword, challenge, secCode, validate).let {
return PassportService.login(username, cipheredPassword, challenge, secCode, validate).orThrow {
LoginException(it)
}.let {
credential.updateAndGet { oldCredential ->
if (oldCredential != null) logger.warn { "Override credential: ${oldCredential.userId}" }
it.toCredential().also {
@@ -67,12 +65,15 @@ class BilibiliClient(credential: Credential? = null) : Closeable {
*
* @return return old credential, null if not logged in
*/
@Throws(RevokeException::class)
suspend fun revoke() = credential.getAndUpdate { oldCredential ->
if (oldCredential != null) {
PassportService.revoke(
oldCredential.accessToken,
oldCredential.cookieMap()
)
).orThrow {
RevokeException(it)
}
logger.debug { "Token revoked: ${oldCredential.userId}" }
}
null

View File

@@ -22,11 +22,17 @@ internal fun String.base64() = Base64.getDecoder().decode(this)
internal fun ByteArray.base64() = String(Base64.getEncoder().encode(this))
internal fun String.rsaEncrypt(publicKey: String) =
X509EncodedKeySpec(publicKey.base64()).let {
internal fun String.rsaEncrypt(publicKey: DER) =
X509EncodedKeySpec(publicKey).let {
KeyFactory.getInstance("RSA").generatePublic(it)
}.let {
Cipher.getInstance("RSA/ECB/PKCS1Padding").apply {
init(Cipher.ENCRYPT_MODE, it)
}
}.doFinal(toByteArray()).base64()
internal typealias PEM = String
internal typealias DER = ByteArray
internal fun PEM.toDER(): DER = split('\n').filterNot { it.startsWith('-') }.joinToString(separator = "").base64()

View File

@@ -3,3 +3,5 @@ package com.hiczp.bilibili.rest.service
interface Ok {
fun ok(): Boolean
}
inline fun <T : Ok> T.orThrow(block: (T) -> Throwable) = if (ok()) this else throw block(this)

View File

@@ -0,0 +1,10 @@
@file:Suppress("CanBeParameter", "MemberVisibilityCanBePrivate")
package com.hiczp.bilibili.rest.service.passport
import com.hiczp.bilibili.rest.service.passport.model.LoginResponse
import com.hiczp.bilibili.rest.service.passport.model.RevokeResponse
class LoginException internal constructor(val loginResponse: LoginResponse) : IllegalStateException(loginResponse.message)
class RevokeException internal constructor(val revokeResponse: RevokeResponse) : IllegalStateException(revokeResponse.message)

View File

@@ -4,10 +4,10 @@ import com.hiczp.bilibili.rest.BilibiliClientInherent
import com.hiczp.bilibili.rest.ktor.appendMissing
import com.hiczp.bilibili.rest.ktor.commonParams
import com.hiczp.bilibili.rest.ktor.logging
import com.hiczp.bilibili.rest.service.Response
import com.hiczp.bilibili.rest.service.passport.model.GetKeyResponse
import com.hiczp.bilibili.rest.service.passport.model.LoginResponse
import com.hiczp.bilibili.rest.service.passport.model.OAuth2Info
import com.hiczp.bilibili.rest.service.passport.model.RevokeResponse
import com.hiczp.caeruleum.annotation.*
import com.hiczp.caeruleum.create
import io.ktor.client.HttpClient
@@ -63,7 +63,7 @@ interface PassportService {
@Field("SESSDATA") sessData: String? = null,
@Field("bili_jct") biliJct: String? = null,
@Field sid: String? = null
): Response
): RevokeResponse
/**
* 将所有 cookie 以 Map 形式传入
@@ -73,7 +73,7 @@ interface PassportService {
suspend fun revoke(
@Field("access_token") accessToken: String,
@FieldMap cookieMap: Map<String, String> = emptyMap()
): Response
): RevokeResponse
/**
* 获取 OAuth2 信息

View File

@@ -1,6 +1,7 @@
package com.hiczp.bilibili.rest.service.passport.model
import com.google.gson.annotations.SerializedName
import com.hiczp.bilibili.rest.ktor.PEM
import com.hiczp.bilibili.rest.service.Ok
data class GetKeyResponse(
@@ -17,7 +18,7 @@ data class GetKeyResponse(
@SerializedName("hash")
val hash: String = "", // a953480d976dd1ba
@SerializedName("key")
val key: String = "" // -----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjb4V7EidX/ym28t2ybo0U6t0n6p4ej8VjqKHg100va6jkNbNTrLQqMCQCAYtXMXXp2Fwkk6WR+12N9zknLjf+C9sx/+l48mjUU8RqahiFD1XT/u2e0m2EN029OhCgkHx3Fc/KlFSIbak93EH/XlYis0w+Xl69GV6klzgxW6d2xQIDAQAB-----END PUBLIC KEY-----
val key: PEM = "" // -----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjb4V7EidX/ym28t2ybo0U6t0n6p4ej8VjqKHg100va6jkNbNTrLQqMCQCAYtXMXXp2Fwkk6WR+12N9zknLjf+C9sx/+l48mjUU8RqahiFD1XT/u2e0m2EN029OhCgkHx3Fc/KlFSIbak93EH/XlYis0w+Xl69GV6klzgxW6d2xQIDAQAB-----END PUBLIC KEY-----
)
override fun ok() = code == 0

View File

@@ -0,0 +1,15 @@
package com.hiczp.bilibili.rest.service.passport.model
import com.google.gson.annotations.SerializedName
import com.hiczp.bilibili.rest.service.Ok
data class RevokeResponse(
@SerializedName("message")
val message: String? = "", // user not login
@SerializedName("ts")
val ts: Long = 0, // 1562653921
@SerializedName("code")
val code: Int = 0 // -101
) : Ok {
override fun ok() = code == 0
}

View File

@@ -39,7 +39,7 @@ class BilibiliClientTest {
runBlocking {
bilibiliClient.login(username, password)
bilibiliClient.credential.value!!.println()
bilibiliClient.revoke()
bilibiliClient.revoke()!!.println()
}
}