feat: HTTP server for authentication

This commit is contained in:
hstyi
2025-10-23 17:23:01 +08:00
committed by GitHub
parent d80a9d48ab
commit d21ae5499a
8 changed files with 235 additions and 179 deletions

View File

@@ -339,6 +339,7 @@ tasks.register<Exec>("jlink") {
"java.security.jgss", "java.security.jgss",
"jdk.crypto.ec", "jdk.crypto.ec",
"jdk.unsupported", "jdk.unsupported",
"jdk.httpserver",
) )
commandLine( commandLine(

View File

@@ -1,6 +1,7 @@
package app.termora.account package app.termora.account
import app.termora.* import app.termora.*
import app.termora.Application.ohMyJson
import app.termora.OptionsPane.Companion.FORM_MARGIN import app.termora.OptionsPane.Companion.FORM_MARGIN
import app.termora.actions.AnAction import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent import app.termora.actions.AnActionEvent
@@ -8,21 +9,36 @@ import app.termora.database.DatabaseManager
import app.termora.plugin.internal.extension.DynamicExtensionHandler import app.termora.plugin.internal.extension.DynamicExtensionHandler
import com.jgoodies.forms.builder.FormBuilder import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout import com.jgoodies.forms.layout.FormLayout
import com.sun.net.httpserver.HttpServer
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import org.apache.commons.codec.binary.Hex
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.time.DateFormatUtils import org.apache.commons.lang3.time.DateFormatUtils
import org.jdesktop.swingx.JXBusyLabel
import org.jdesktop.swingx.JXHyperlink import org.jdesktop.swingx.JXHyperlink
import org.slf4j.LoggerFactory
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.CardLayout
import java.net.InetSocketAddress
import java.net.URI import java.net.URI
import java.net.URLEncoder
import java.util.* import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import javax.swing.* import javax.swing.*
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable { class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
companion object {
private val log = LoggerFactory.getLogger(AccountOption::class.java)
}
private val owner get() = SwingUtilities.getWindowAncestor(this) private val owner get() = SwingUtilities.getWindowAncestor(this)
private val databaseManager get() = DatabaseManager.getInstance() private val databaseManager get() = DatabaseManager.getInstance()
@@ -30,18 +46,31 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
private val accountProperties get() = AccountProperties.getInstance() private val accountProperties get() = AccountProperties.getInstance()
private val userInfoPanel = JPanel(BorderLayout()) private val userInfoPanel = JPanel(BorderLayout())
private val lastSynchronizationOnLabel = JLabel() private val lastSynchronizationOnLabel = JLabel()
private val serverManager get() = ServerManager.getInstance()
private val cardLayout = CardLayout()
private val contentPanel = JPanel(cardLayout)
private val loginPanel = JPanel(BorderLayout())
private val busyLabel = JXBusyLabel()
private var httpServer: HttpServer? = null
init { init {
initView() initView()
initEvents() initEvents()
} }
private fun initView() { private fun initView() {
refreshUserInfoPanel() refreshUserInfoPanel()
add(userInfoPanel, BorderLayout.CENTER) refreshLoginPanel()
}
contentPanel.add(userInfoPanel, "UserInfo")
contentPanel.add(loginPanel, "Login")
cardLayout.show(contentPanel, "UserInfo")
add(contentPanel, BorderLayout.CENTER)
}
private fun initEvents() { private fun initEvents() {
// 服务器签名发生变更 // 服务器签名发生变更
@@ -99,11 +128,7 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
planBox.add(Box.createHorizontalStrut(16)) planBox.add(Box.createHorizontalStrut(16))
val upgrade = JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.upgrade")) { val upgrade = JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.upgrade")) {
override fun actionPerformed(evt: AnActionEvent) { override fun actionPerformed(evt: AnActionEvent) {
if (I18n.isChinaMainland()) { Application.browse(URI.create("${accountManager.getServer()}/v1/client/redirect?to=upgrade&version=${Application.getVersion()}"))
Application.browse(URI.create("https://www.termora.cn/pricing?version=${Application.getVersion()}"))
} else {
Application.browse(URI.create("https://www.termora.app/pricing?version=${Application.getVersion()}"))
}
} }
}) })
upgrade.isFocusable = false upgrade.isFocusable = false
@@ -145,6 +170,29 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
.build() .build()
} }
private fun getLoginComponent(): JComponent {
val layout = FormLayout(
"default:grow",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
)
val cancelBtn = JXHyperlink(object : AnAction(I18n.getString("termora.cancel")) {
override fun actionPerformed(evt: AnActionEvent) {
httpServer?.stop(0)
cardLayout.show(contentPanel, "UserInfo")
}
})
val tipLabel = JLabel(I18n.getString("termora.settings.account.wait-login"))
tipLabel.foreground = UIManager.getColor("TextField.placeholderForeground")
return FormBuilder.create().layout(layout).debug(false).padding("10dlu,0,0,0")
.add(busyLabel).xy(1, 1, "center, fill")
.add(tipLabel).xy(1, 3, "center, fill")
.add(cancelBtn).xy(1, 5, "center, fill")
.build()
}
private fun createActionPanel(isFreePlan: Boolean): JComponent { private fun createActionPanel(isFreePlan: Boolean): JComponent {
val actionBox = Box.createHorizontalBox() val actionBox = Box.createHorizontalBox()
actionBox.add(Box.createHorizontalGlue()) actionBox.add(Box.createHorizontalGlue())
@@ -219,11 +267,139 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
return actionBox return actionBox
} }
private fun showLoginPanel() {
refreshLoginPanel()
busyLabel.isBusy = true
cardLayout.show(contentPanel, "Login")
}
private fun onLogin() { private fun onLogin() {
httpServer?.stop(0)
val dialog = LoginServerDialog(owner) val dialog = LoginServerDialog(owner)
dialog.isVisible = true dialog.isVisible = true
val server = dialog.server ?: return
showLoginPanel()
onLogin(server)
} }
private fun onLogin(server: Server) {
val httpServer = HttpServer.create(InetSocketAddress("127.0.0.1", 0), 0)
.apply { httpServer = this }
val future = processLogin(server, httpServer)
val loginJob = swingCoroutineScope.launch(Dispatchers.IO) {
try {
val loginResult = future.get(5, TimeUnit.MINUTES)
serverManager.login(server, loginResult.refreshToken, loginResult.password)
} catch (e: Exception) {
if (log.isErrorEnabled) log.error(e.message, e)
withContext(Dispatchers.Swing) {
OptionPane.showMessageDialog(
owner,
StringUtils.defaultIfBlank(
e.message ?: StringUtils.EMPTY,
I18n.getString("termora.settings.account.login-failed")
),
messageType = JOptionPane.ERROR_MESSAGE,
)
}
} finally {
withContext(Dispatchers.Swing) { cardLayout.show(contentPanel, "UserInfo") }
httpServer.stop(0)
}
}
Disposer.register(this, object : Disposable {
override fun dispose() {
loginJob.cancel()
httpServer.stop(0)
}
})
}
override fun dispose() {
busyLabel.isBusy = false
super.dispose()
}
private fun processLogin(server: Server, httpServer: HttpServer): CompletableFuture<LoginResult> {
val keypair = RSA.generateKeyPair(2048)
val future = CompletableFuture<LoginResult>()
httpServer.createContext("/callback") { exchange ->
val method = exchange.requestMethod
if (method.equals("OPTIONS", ignoreCase = true)) {
exchange.responseHeaders.add("Access-Control-Allow-Origin", "*")
exchange.responseHeaders.add("Access-Control-Allow-Methods", "POST, OPTIONS")
exchange.responseHeaders.add("Access-Control-Allow-Headers", "Content-Type")
exchange.sendResponseHeaders(204, -1)
} else {
var loginResult: LoginResult? = null
if (method.equals("POST", ignoreCase = true)) {
try {
val text = String(exchange.requestBody.readAllBytes())
loginResult = ohMyJson.decodeFromString<LoginResult>(text)
val secretKey = RSA.decrypt(keypair.private, Hex.decodeHex(loginResult.secretKey))
val secretIv = RSA.decrypt(keypair.private, Hex.decodeHex(loginResult.secretIv))
val password = AES.CBC.decrypt(secretKey, secretIv, Hex.decodeHex(loginResult.password))
val refreshToken = AES.CBC.decrypt(
secretKey, secretIv, Hex.decodeHex(loginResult.refreshToken)
)
loginResult = loginResult.copy(
password = String(password),
refreshToken = String(refreshToken)
)
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
}
val response = "OK".toByteArray()
exchange.responseHeaders.add("Access-Control-Allow-Origin", "*")
exchange.sendResponseHeaders(200, response.size.toLong())
exchange.responseBody.use { it.write(response) }
if (loginResult != null) {
future.complete(loginResult)
}
}
IOUtils.closeQuietly { exchange.close() }
}
httpServer.start()
val sb = StringBuilder()
val redirect = StringBuilder()
redirect.append("/device?callback=").append("http://127.0.0.1:${httpServer.address.port}/callback")
redirect.append("&from=device&publicKey=").append(keypair.public.encoded.toHexString())
redirect.append("&format=hex&device=termora&device-version=").append(Application.getVersion())
sb.append(server.server)
sb.append("/v1/client/redirect?to=login&from=device")
sb.append("&redirect=").append(URLEncoder.encode(redirect.toString(), Charsets.UTF_8))
Application.browse(URI.create(sb.toString()))
return future
}
@Serializable
private data class LoginResult(
val password: String,
val refreshToken: String,
val secretKey: String,
val secretIv: String,
)
private fun refreshUserInfoPanel() { private fun refreshUserInfoPanel() {
userInfoPanel.removeAll() userInfoPanel.removeAll()
userInfoPanel.add(getCenterComponent(), BorderLayout.CENTER) userInfoPanel.add(getCenterComponent(), BorderLayout.CENTER)
@@ -231,6 +407,13 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
userInfoPanel.repaint() userInfoPanel.repaint()
} }
private fun refreshLoginPanel() {
loginPanel.removeAll()
loginPanel.add(getLoginComponent(), BorderLayout.CENTER)
loginPanel.revalidate()
loginPanel.repaint()
}
override fun getIcon(isSelected: Boolean): Icon { override fun getIcon(isSelected: Boolean): Icon {
return Icons.user return Icons.user
} }

View File

@@ -9,12 +9,6 @@ import app.termora.database.DatabaseManager
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.jgoodies.forms.builder.FormBuilder import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout import com.jgoodies.forms.layout.FormLayout
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Request
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.jdesktop.swingx.JXHyperlink import org.jdesktop.swingx.JXHyperlink
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -24,12 +18,10 @@ import java.awt.Window
import java.awt.event.WindowAdapter import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent import java.awt.event.WindowEvent
import java.net.URI import java.net.URI
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.* import javax.swing.*
import javax.swing.event.ListDataEvent import javax.swing.event.ListDataEvent
import javax.swing.event.ListDataListener import javax.swing.event.ListDataListener
import kotlin.math.max import kotlin.math.max
import kotlin.time.Duration.Companion.milliseconds
class LoginServerDialog(owner: Window) : DialogWrapper(owner) { class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
companion object { companion object {
@@ -37,18 +29,14 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
} }
private val serverComboBox = OutlineComboBox<Server>() private val serverComboBox = OutlineComboBox<Server>()
private val usernameTextField = OutlineTextField(128)
private val passwordField = OutlinePasswordField()
private val mfaTextField = OutlineTextField(128)
private val okAction = OkAction(I18n.getString("termora.settings.account.login")) private val okAction = OkAction(I18n.getString("termora.settings.account.login"))
private val cancelAction = super.createCancelAction() private val cancelAction = super.createCancelAction()
private val cancelButton = super.createJButtonForAction(cancelAction) private val cancelButton = super.createJButtonForAction(cancelAction)
private val isLoggingIn = AtomicBoolean(false)
private val singaporeServer = private val singaporeServer =
Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app") Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app")
private val chinaServer = private val chinaServer =
Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn") Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn")
private val serverManager get() = ServerManager.getInstance() var server: Server? = null
init { init {
isModal = true isModal = true
@@ -60,12 +48,10 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
size = Dimension(max(preferredSize.width, UIManager.getInt("Dialog.width") - 250), preferredSize.height) size = Dimension(max(preferredSize.width, UIManager.getInt("Dialog.width") - 250), preferredSize.height)
setLocationRelativeTo(owner) setLocationRelativeTo(owner)
passwordField.putClientProperty(FlatClientProperties.STYLE, mapOf("showCapsLock" to true))
addWindowListener(object : WindowAdapter() { addWindowListener(object : WindowAdapter() {
override fun windowOpened(e: WindowEvent) { override fun windowOpened(e: WindowEvent) {
removeWindowListener(this) removeWindowListener(this)
usernameTextField.requestFocus()
} }
}) })
} }
@@ -73,7 +59,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
override fun createCenterPanel(): JComponent { override fun createCenterPanel(): JComponent {
val layout = FormLayout( val layout = FormLayout(
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref", "left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref",
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN" "pref, $FORM_MARGIN"
) )
var rows = 1 var rows = 1
@@ -90,7 +76,6 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
serverComboBox.addItem(Server(server.name, server.server)) serverComboBox.addItem(Server(server.name, server.server))
} }
mfaTextField.placeholderText = I18n.getString("termora.settings.account.mfa")
serverComboBox.renderer = object : DefaultListCellRenderer() { serverComboBox.renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent( override fun getListCellRendererComponent(
@@ -153,40 +138,6 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
} }
} }
val registerAction = object : AnAction(I18n.getString("termora.settings.account.register")) {
override fun actionPerformed(evt: AnActionEvent) {
val server = serverComboBox.selectedItem as Server?
if (server == null) {
serverComboBox.outline = FlatClientProperties.OUTLINE_ERROR
serverComboBox.requestFocusInWindow()
return
}
try {
val text = AccountHttp.execute(
AccountHttp.client, Request.Builder()
.get().url("${server.server}/v1/client/system").build()
)
val json = runCatching { ohMyJson.decodeFromString<JsonObject>(text) }.getOrNull()
val allowRegister = json?.get("register")?.jsonPrimitive?.boolean ?: false
if (allowRegister.not()) {
throw IllegalStateException(I18n.getString("termora.settings.account.not-support-register"))
}
Application.browse(URI.create("${server.server}/v1/client/redirect?to=register&from=${Application.getName()}"))
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
OptionPane.showMessageDialog(
dialog,
e.message ?: I18n.getString("termora.settings.account.not-support-register"),
messageType = JOptionPane.ERROR_MESSAGE
)
}
}
}
fun refreshButton() { fun refreshButton() {
if (serverComboBox.selectedItem == singaporeServer || serverComboBox.selectedItem == chinaServer || serverComboBox.itemCount < 1) { if (serverComboBox.selectedItem == singaporeServer || serverComboBox.selectedItem == chinaServer || serverComboBox.itemCount < 1) {
newAction.name = I18n.getString("termora.welcome.contextmenu.new") newAction.name = I18n.getString("termora.welcome.contextmenu.new")
@@ -214,21 +165,11 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
}) })
val registerLink = JXHyperlink(registerAction)
registerLink.isFocusable = false
return FormBuilder.create().layout(layout).debug(false).padding("0dlu, $FORM_MARGIN, 0dlu, $FORM_MARGIN") return FormBuilder.create().layout(layout).debug(false).padding("0dlu, $FORM_MARGIN, 0dlu, $FORM_MARGIN")
.add("${I18n.getString("termora.settings.account.server")}:").xy(1, rows) .add("${I18n.getString("termora.settings.account.server")}:").xy(1, rows)
.add(serverComboBox).xy(3, rows) .add(serverComboBox).xy(3, rows)
.add(newServer).xy(5, rows).apply { rows += step } .add(newServer).xy(5, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.account")}:").xy(1, rows)
.add(usernameTextField).xy(3, rows)
.add(registerLink).xy(5, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
.add(passwordField).xy(3, rows).apply { rows += step }
.add("MFA:").xy(1, rows)
.add(mfaTextField).xy(3, rows).apply { rows += step }
.build() .build()
} }
@@ -315,95 +256,21 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
} }
override fun doOKAction() { override fun doOKAction() {
if (isLoggingIn.get()) return
val server = serverComboBox.selectedItem as? Server server = serverComboBox.selectedItem as? Server
if (server == null) { if (server == null) {
serverComboBox.outline = FlatClientProperties.OUTLINE_ERROR serverComboBox.outline = FlatClientProperties.OUTLINE_ERROR
serverComboBox.requestFocusInWindow() serverComboBox.requestFocusInWindow()
return return
} }
if (usernameTextField.text.isBlank()) {
usernameTextField.outline = FlatClientProperties.OUTLINE_ERROR
usernameTextField.requestFocusInWindow()
return
} else if (passwordField.password.isEmpty()) {
passwordField.outline = FlatClientProperties.OUTLINE_ERROR
passwordField.requestFocusInWindow()
return
}
if (isLoggingIn.compareAndSet(false, true)) {
okAction.isEnabled = false
usernameTextField.isEnabled = false
passwordField.isEnabled = false
mfaTextField.isEnabled = false
serverComboBox.isEnabled = false
cancelButton.isVisible = false
onLogin(server)
return
}
super.doOKAction() super.doOKAction()
} }
private fun onLogin(server: Server) {
val job = swingCoroutineScope.launch(Dispatchers.IO) {
var c = 0
while (isActive) {
if (++c > 3) c = 0
okAction.name = I18n.getString("termora.settings.account.login") + ".".repeat(c)
delay(350.milliseconds)
}
}
val loginJob = swingCoroutineScope.launch(Dispatchers.IO) {
try {
serverManager.login(
server, usernameTextField.text,
String(passwordField.password), mfaTextField.text.trim()
)
withContext(Dispatchers.Swing) {
super.doOKAction()
}
} catch (e: Exception) {
if (log.isErrorEnabled) log.error(e.message, e)
withContext(Dispatchers.Swing) {
OptionPane.showMessageDialog(
this@LoginServerDialog,
StringUtils.defaultIfBlank(
e.message ?: StringUtils.EMPTY,
I18n.getString("termora.settings.account.login-failed")
),
messageType = JOptionPane.ERROR_MESSAGE,
)
}
} finally {
job.cancel()
withContext(Dispatchers.Swing) {
okAction.name = I18n.getString("termora.settings.account.login")
okAction.isEnabled = true
usernameTextField.isEnabled = true
passwordField.isEnabled = true
serverComboBox.isEnabled = true
cancelButton.isVisible = true
mfaTextField.isEnabled = true
}
isLoggingIn.compareAndSet(true, false)
}
}
Disposer.register(disposable, object : Disposable {
override fun dispose() {
if (loginJob.isActive)
loginJob.cancel()
}
})
}
override fun doCancelAction() { override fun doCancelAction() {
if (isLoggingIn.get()) return server = null
super.doCancelAction() super.doCancelAction()
} }
} }

View File

@@ -1,7 +1,10 @@
package app.termora.account package app.termora.account
import app.termora.* import app.termora.AES
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.ApplicationScope
import app.termora.PBKDF2
import app.termora.RSA
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
@@ -9,7 +12,6 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import org.apache.commons.codec.binary.Base64 import org.apache.commons.codec.binary.Base64
import org.apache.commons.codec.digest.DigestUtils
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
class ServerManager private constructor() { class ServerManager private constructor() {
@@ -28,7 +30,7 @@ class ServerManager private constructor() {
/** /**
* 登录,不报错就是登录成功 * 登录,不报错就是登录成功
*/ */
fun login(server: Server, username: String, password: String, mfa: String) { fun login(server: Server, refreshToken: String, password: String) {
if (accountManager.isLocally().not()) { if (accountManager.isLocally().not()) {
throw IllegalStateException("Already logged in") throw IllegalStateException("Already logged in")
@@ -39,25 +41,25 @@ class ServerManager private constructor() {
} }
try { try {
doLogin(server, username, password, mfa) doLogin(server, refreshToken, password)
} finally { } finally {
isLoggingIn.compareAndSet(true, false) isLoggingIn.compareAndSet(true, false)
} }
} }
private fun doLogin(server: Server, username: String, password: String, mfa: String) { private fun doLogin(server: Server, refreshToken: String, password: String) {
// 服务器信息 // 服务器信息
val serverInfo = getServerInfo(server) val serverInfo = getServerInfo(server)
// call login // call login
val loginResponse = callLogin(serverInfo, server, username, password, mfa) val loginResponse = callToken(server, refreshToken)
// call me // call me
val meResponse = callMe(server.server, loginResponse.accessToken) val meResponse = callMe(server.server, loginResponse.accessToken)
// 解密 // 解密
val salt = "${serverInfo.salt}:${username}".toByteArray() val salt = "${serverInfo.salt}:${meResponse.email}".toByteArray()
val privateKeySecureKey = PBKDF2.hash(salt, password.toCharArray(), 1024, 256) val privateKeySecureKey = PBKDF2.hash(salt, password.toCharArray(), 1024, 256)
val privateKeySecureIv = PBKDF2.hash(salt, password.toCharArray(), 1024, 128) val privateKeySecureIv = PBKDF2.hash(salt, password.toCharArray(), 1024, 128)
val privateKeyEncoded = AES.CBC.decrypt( val privateKeyEncoded = AES.CBC.decrypt(
@@ -106,29 +108,19 @@ class ServerManager private constructor() {
return ohMyJson.decodeFromString<ServerInfo>(AccountHttp.execute(request = request)) return ohMyJson.decodeFromString<ServerInfo>(AccountHttp.execute(request = request))
} }
private fun callLogin( private fun callToken(
serverInfo: ServerInfo,
server: Server, server: Server,
username: String, refreshToken: String,
password: String,
mfa: String
): LoginResponse { ): LoginResponse {
val body = ohMyJson.encodeToString(mapOf("refreshToken" to refreshToken))
val passwordHex = DigestUtils.sha256Hex("${serverInfo.salt}:${username}:${password}")
val requestBody = ohMyJson.encodeToString(mapOf("email" to username, "password" to passwordHex, "mfa" to mfa))
.toRequestBody("application/json".toMediaType()) .toRequestBody("application/json".toMediaType())
val request = Request.Builder().url("${server.server}/v1/token")
val request = Request.Builder() .header("Authorization", "Bearer $refreshToken")
.url("${server.server}/v1/login") .post(body)
.post(requestBody)
.build() .build()
val response = AccountHttp.client.newCall(request).execute() val response = AccountHttp.client.newCall(request).execute()
val text = response.use { response.body.use { it?.string() } } val text = response.use { response.body.use { it.string() } }
if (text == null) {
throw ResponseException(response.code, response)
}
if (response.isSuccessful.not()) { if (response.isSuccessful.not()) {
val message = ohMyJson.parseToJsonElement(text).jsonObject["message"]?.jsonPrimitive?.content val message = ohMyJson.parseToJsonElement(text).jsonObject["message"]?.jsonPrimitive?.content

View File

@@ -89,11 +89,9 @@ termora.settings.plugin.install-from-disk-warning=<b>{0}</b> plugin will have ac
termora.settings.plugin.not-compatible=The plugin <b>{0}</b> is incompatible with the current version. Please reinstall <b>{0}</b> termora.settings.plugin.not-compatible=The plugin <b>{0}</b> is incompatible with the current version. Please reinstall <b>{0}</b>
termora.settings.account=Account termora.settings.account=Account
termora.settings.account.register=Register
termora.settings.account.not-support-register=This server does not support account registration
termora.settings.account.login=Log in termora.settings.account.login=Log in
termora.settings.account.server=Server termora.settings.account.server=Server
termora.settings.account.mfa=MFA is optional termora.settings.account.wait-login=Waiting for login in the default browser...
termora.settings.account.locally=locally termora.settings.account.locally=locally
termora.settings.account.lifetime=Lifetime termora.settings.account.lifetime=Lifetime
termora.settings.account.upgrade=Upgrade termora.settings.account.upgrade=Upgrade

View File

@@ -49,7 +49,26 @@ termora.setting.security.enter-password-again=Повторите пароль
termora.setting.security.password-is-different=Пароли отличаются termora.setting.security.password-is-different=Пароли отличаются
termora.setting.security.mnemonic-note=Сохраните мнемоническую фразу в надежном месте, она может помочь восстановить данные, если вы забудете пароль termora.setting.security.mnemonic-note=Сохраните мнемоническую фразу в надежном месте, она может помочь восстановить данные, если вы забудете пароль
termora.settings.account=Учётная запись
termora.settings.account.login=Войти
termora.settings.account.server=Сервер
termora.settings.account.wait-login=Ожидание входа в браузере по умолчанию...
termora.settings.account.locally=локально
termora.settings.account.lifetime=Время действия
termora.settings.account.upgrade=Обновить
termora.settings.account.verify=Подтвердить
termora.settings.account.subscription=Подписка
termora.settings.account.valid-to=Действительна до
termora.settings.account.synchronization-on=Синхронизация вкл
termora.settings.account.sync-now = Синхронизировать сейчас
termora.settings.account.logout = Выйти
termora.settings.account.logout-confirm = Вы уверены, что хотите выйти?
termora.settings.account.unsynced-logout-confirm = Несинхронизировано Вы уверены, что хотите выйти?
termora.settings.account.server-singapore = Сингапур
termora.settings.account.server-china = Материковый Китай
termora.settings.account.new-server = Новый сервер
termora.settings.account.deploy-server = Развернуть
termora.settings.account.login-failed = Не удалось войти, повторите попытку позже
termora.settings.terminal=Терминал termora.settings.terminal=Терминал

View File

@@ -103,10 +103,8 @@ termora.settings.plugin.not-compatible=插件 <b>{0}</b> 与当前版本不兼
termora.settings.account=账号 termora.settings.account=账号
termora.settings.account.login=登录 termora.settings.account.login=登录
termora.settings.account.register=注册
termora.settings.account.not-support-register=该服务器不支持注册账号
termora.settings.account.server=服务器 termora.settings.account.server=服务器
termora.settings.account.mfa=多因素验证是可选的 termora.settings.account.wait-login=正在等待默认浏览器中登录...
termora.settings.account.locally=本地的 termora.settings.account.locally=本地的
termora.settings.account.lifetime=长期 termora.settings.account.lifetime=长期
termora.settings.account.verify=验证 termora.settings.account.verify=验证

View File

@@ -114,10 +114,8 @@ termora.settings.plugin.not-compatible=插件 <b>{0}</b> 與目前版本不相
termora.settings.account=帳號 termora.settings.account=帳號
termora.settings.account.login=登入 termora.settings.account.login=登入
termora.settings.account.register=註冊
termora.settings.account.not-support-register=此伺服器不支援註冊帳號
termora.settings.account.server=伺服器 termora.settings.account.server=伺服器
termora.settings.account.mfa=多因素驗證是可選的 termora.settings.account.wait-login=正在等待預設瀏覽器登入...
termora.settings.account.locally=本地的 termora.settings.account.locally=本地的
termora.settings.account.lifetime=長期 termora.settings.account.lifetime=長期
termora.settings.account.verify=驗證 termora.settings.account.verify=驗證