mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
Compare commits
25 Commits
2.0.0-beta
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
124acb2d7d | ||
|
|
5110595404 | ||
|
|
034e0939be | ||
|
|
4ccfa82c8a | ||
|
|
d21ae5499a | ||
|
|
d80a9d48ab | ||
|
|
b305d6fd34 | ||
|
|
756fd305d1 | ||
|
|
f9549fbb7d | ||
|
|
e18b454fcc | ||
|
|
4f4ccfa7d4 | ||
|
|
5dfd5fefb2 | ||
|
|
a7ea4c70d2 | ||
|
|
b7796f58f0 | ||
|
|
c7bedc57e0 | ||
|
|
935f305ada | ||
|
|
8cf47a7ca1 | ||
|
|
c6c5ad711d | ||
|
|
5fc76d955a | ||
|
|
0aabe1b0dc | ||
|
|
820c4274e7 | ||
|
|
fcec30d70a | ||
|
|
f6dc0098f7 | ||
|
|
ca7b30bdb0 | ||
|
|
f73e7f4214 |
4
.github/workflows/linux.yml
vendored
4
.github/workflows/linux.yml
vendored
@@ -3,8 +3,8 @@ name: Linux
|
||||
on: [ push, pull_request ]
|
||||
|
||||
env:
|
||||
JBR_MAJOR: 21.0.7
|
||||
JBR_PATCH: b1038.58
|
||||
JBR_MAJOR: 21.0.8
|
||||
JBR_PATCH: b1138.52
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
4
.github/workflows/osx.yml
vendored
4
.github/workflows/osx.yml
vendored
@@ -8,8 +8,8 @@ env:
|
||||
# 只有发布版本时才需要公证
|
||||
TERMORA_MAC_NOTARY: "${{ startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' }}"
|
||||
TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.TERMORA_MAC_NOTARY_KEYCHAIN_PROFILE }}
|
||||
JBR_MAJOR: 21.0.7
|
||||
JBR_PATCH: b1038.58
|
||||
JBR_MAJOR: 21.0.8
|
||||
JBR_PATCH: b1138.52
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
4
.github/workflows/windows.yml
vendored
4
.github/workflows/windows.yml
vendored
@@ -3,8 +3,8 @@ name: Windows
|
||||
on: [ push, pull_request ]
|
||||
|
||||
env:
|
||||
JBR_MAJOR: 21.0.7
|
||||
JBR_PATCH: b1038.58
|
||||
JBR_MAJOR: 21.0.8
|
||||
JBR_PATCH: b1138.52
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -339,6 +339,7 @@ tasks.register<Exec>("jlink") {
|
||||
"java.security.jgss",
|
||||
"jdk.crypto.ec",
|
||||
"jdk.unsupported",
|
||||
"jdk.httpserver",
|
||||
)
|
||||
|
||||
commandLine(
|
||||
|
||||
@@ -4,10 +4,10 @@ slf4j = "2.0.17"
|
||||
pty4j = "0.13.10"
|
||||
tinylog = "2.7.0"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
flatlaf = "3.6.1"
|
||||
flatlaf = "3.6.2"
|
||||
kotlinx-serialization-json = "1.9.0"
|
||||
commons-codec = "1.19.0"
|
||||
commons-lang3 = "3.18.0"
|
||||
commons-lang3 = "3.19.0"
|
||||
commons-csv = "1.14.1"
|
||||
commons-net = "3.12.0"
|
||||
commons-text = "1.14.0"
|
||||
@@ -16,18 +16,18 @@ commons-vfs2 = "2.10.0"
|
||||
swingx = "1.6.5-1"
|
||||
jgoodies-forms = "1.9.0"
|
||||
jfa = "1.2.0"
|
||||
oshi = "6.8.1"
|
||||
oshi = "6.9.0"
|
||||
versioncompare = "1.4.1"
|
||||
jna = "5.17.0"
|
||||
jna = "5.18.1"
|
||||
jSystemThemeDetector = "3.9.1"
|
||||
commons-io = "2.20.0"
|
||||
jbr-api = "17.1.10.1"
|
||||
hutool = "5.8.40"
|
||||
jsch = "2.27.3"
|
||||
okhttp = "5.1.0"
|
||||
okhttp = "5.2.1"
|
||||
sshj = "0.39.0"
|
||||
sshd-core = "2.15.0"
|
||||
jgit = "7.2.0.202503040940-r"
|
||||
jgit = "7.4.0.202509020913-r"
|
||||
commonmark = "0.26.0"
|
||||
jnafilechooser = "1.1.2"
|
||||
xodus = "2.0.1"
|
||||
@@ -35,16 +35,16 @@ bip39 = "1.0.9"
|
||||
colorpicker = "2.0.1"
|
||||
rhino = "1.8.0"
|
||||
delight-rhino-sandbox = "0.0.17"
|
||||
testcontainers = "1.21.3"
|
||||
testcontainers = "2.0.0"
|
||||
mixpanel = "1.5.3"
|
||||
jSerialComm = "2.11.2"
|
||||
ini4j = "0.5.5-2"
|
||||
restart4j = "0.0.1"
|
||||
eddsa = "0.3.0"
|
||||
exposed = "1.0.0-rc-1"
|
||||
h2 = "2.3.232"
|
||||
exposed = "1.0.0-rc-2"
|
||||
h2 = "2.4.240"
|
||||
sqlite = "3.50.3.0"
|
||||
jug = "5.1.0"
|
||||
jug = "5.1.1"
|
||||
semver4j = "6.0.0"
|
||||
jsvg = "2.0.0"
|
||||
dom4j = "2.2.0"
|
||||
|
||||
@@ -8,7 +8,7 @@ project.version = "0.0.4"
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
implementation("com.qcloud:cos_api:5.6.255")
|
||||
implementation("com.qcloud:cos_api:5.6.257")
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
|
||||
|
||||
|
||||
project.version = "0.0.7"
|
||||
project.version = "0.0.8"
|
||||
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package app.termora.plugins.editor
|
||||
|
||||
import app.termora.DocumentAdaptor
|
||||
import app.termora.DynamicColor
|
||||
import app.termora.EnableManager
|
||||
import app.termora.Icons
|
||||
import app.termora.*
|
||||
import app.termora.database.DatabaseManager
|
||||
import com.formdev.flatlaf.FlatLaf
|
||||
import com.formdev.flatlaf.extras.components.FlatTextField
|
||||
@@ -39,6 +36,10 @@ class EditorPanel(private val window: JFrame, private val file: File) : JPanel(B
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(EditorPanel::class.java)
|
||||
private val saveIcon = DynamicIcon(
|
||||
"icons/save.svg", "icons/save_dark.svg",
|
||||
loader = EditorPlugin::class.java.classLoader
|
||||
)
|
||||
}
|
||||
|
||||
private var text = file.readText(Charsets.UTF_8)
|
||||
@@ -54,6 +55,7 @@ class EditorPanel(private val window: JFrame, private val file: File) : JPanel(B
|
||||
private val prevBtn = JButton(Icons.up)
|
||||
private val context = SearchContext()
|
||||
private val softWrapBtn = JToggleButton(Icons.softWrap)
|
||||
private val saveBtn = JButton(saveIcon)
|
||||
private val scrollUpBtn = JButton(Icons.scrollUp)
|
||||
private val scrollEndBtn = JButton(Icons.scrollDown)
|
||||
private val prettyBtn = JButton(Icons.reformatCode)
|
||||
@@ -141,11 +143,18 @@ class EditorPanel(private val window: JFrame, private val file: File) : JPanel(B
|
||||
)
|
||||
|
||||
toolbar.orientation = VERTICAL
|
||||
toolbar.add(saveBtn)
|
||||
toolbar.add(scrollUpBtn)
|
||||
toolbar.add(prettyBtn)
|
||||
toolbar.add(softWrapBtn)
|
||||
toolbar.add(scrollEndBtn)
|
||||
|
||||
saveBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.save")
|
||||
scrollUpBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.first-line")
|
||||
scrollEndBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.last-line")
|
||||
softWrapBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.soft-wrap")
|
||||
prettyBtn.toolTipText = EditorI18n.getString("termora.plugins.editor.format")
|
||||
|
||||
val viewPanel = JPanel(BorderLayout())
|
||||
viewPanel.add(scrollPane, BorderLayout.CENTER)
|
||||
viewPanel.add(toolbar, BorderLayout.EAST)
|
||||
@@ -211,6 +220,8 @@ class EditorPanel(private val window: JFrame, private val file: File) : JPanel(B
|
||||
}
|
||||
})
|
||||
|
||||
saveBtn.addActionListener(textArea.actionMap.get("Save"))
|
||||
|
||||
textArea.actionMap.put("Format", object : AbstractAction() {
|
||||
override fun actionPerformed(e: ActionEvent) {
|
||||
format()
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
termora.plugins.editor.not-save=The file has not been saved. Are you sure you want to exit?
|
||||
|
||||
termora.plugins.editor.save=Save
|
||||
termora.plugins.editor.first-line=Jump to first line
|
||||
termora.plugins.editor.last-line=Jump to last line
|
||||
termora.plugins.editor.soft-wrap=Soft-wrap
|
||||
termora.plugins.editor.format=Format
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
termora.plugins.editor.not-save=Файл не сохранён. Вы уверены, что хотите выйти?
|
||||
termora.plugins.editor.not-save=Файл не сохранён. Вы уверены, что хотите выйти?
|
||||
termora.plugins.editor.save=Сохранить
|
||||
termora.plugins.editor.first-line=Перейти на первую строку
|
||||
termora.plugins.editor.last-line=Перейти на последнюю строку
|
||||
termora.plugins.editor.soft-wrap=Мягкий перенос
|
||||
termora.plugins.editor.format=Формат
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
termora.plugins.editor.not-save=文件尚未保存,你确定要退出吗?
|
||||
termora.plugins.editor.save=保存
|
||||
termora.plugins.editor.first-line=跳转到第一行
|
||||
termora.plugins.editor.last-line=跳转到最后一行
|
||||
termora.plugins.editor.soft-wrap=自动换行
|
||||
termora.plugins.editor.format=格式化
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
termora.plugins.editor.not-save=檔案尚未儲存,你確定要退出嗎?
|
||||
termora.plugins.editor.not-save=檔案尚未儲存,你確定要退出嗎?
|
||||
termora.plugins.editor.save=儲存
|
||||
termora.plugins.editor.first-line=跳到第一行
|
||||
termora.plugins.editor.last-line=跳到最後一行
|
||||
termora.plugins.editor.soft-wrap=自動換行
|
||||
termora.plugins.editor.format=格式化
|
||||
|
||||
4
plugins/editor/src/main/resources/icons/save.svg
Normal file
4
plugins/editor/src/main/resources/icons/save.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.5 3V5.5H10.5V3M4.5 13V9.5H11.5V13M2.5 13.5V2.5H11.5L13.5 4.5V13.5H2.5Z" stroke="#6C707E" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 357 B |
4
plugins/editor/src/main/resources/icons/save_dark.svg
Normal file
4
plugins/editor/src/main/resources/icons/save_dark.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.5 3V5.5H10.5V3M4.5 13V9.5H11.5V13M2.5 13.5V2.5H11.5L13.5 4.5V13.5H2.5Z" stroke="#CED0D6" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 357 B |
@@ -9,7 +9,7 @@ dependencies {
|
||||
compileOnly(project(":"))
|
||||
implementation("com.maxmind.geoip2:geoip2:4.4.0")
|
||||
// https://github.com/hstyi/geolite2
|
||||
implementation("com.github.hstyi:geolite2:v1.0-202508180058")
|
||||
implementation("com.github.hstyi:geolite2:v1.0-202510200054")
|
||||
}
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
|
||||
@@ -13,7 +13,7 @@ dependencies {
|
||||
testImplementation(libs.testcontainers.junit.jupiter)
|
||||
testImplementation(project(":"))
|
||||
|
||||
implementation("io.minio:minio:8.5.17")
|
||||
implementation("io.minio:minio:8.6.0")
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
|
||||
@@ -874,6 +874,8 @@ class NordDarkLaf : FlatPropertiesLaf("Nord Dark", Properties().apply {
|
||||
TerminalColor.Basic.SELECTION_BACKGROUND,
|
||||
TerminalColor.Cursor.BACKGROUND -> 0xeceff4
|
||||
|
||||
TerminalColor.Basic.SELECTION_FOREGROUND -> 0x3b4252
|
||||
|
||||
TerminalColor.Basic.FOREGROUND -> 0xd8dee9
|
||||
|
||||
|
||||
|
||||
@@ -170,16 +170,18 @@ class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProv
|
||||
|
||||
filterableTreeModel.addFilter(object : Filter {
|
||||
override fun filter(node: Any): Boolean {
|
||||
val text = searchTextField.text
|
||||
val text = searchTextField.text.trim()
|
||||
if (text.isBlank()) return true
|
||||
if (node !is HostTreeNode) return false
|
||||
if (node is TeamTreeNode || node.id == "0") return true
|
||||
return node.host.name.contains(text) || node.host.host.contains(text)
|
||||
|| node.host.username.contains(text)
|
||||
return node.host.name.contains(text, ignoreCase = true)
|
||||
|| node.host.host.contains(text, ignoreCase = true)
|
||||
|| node.host.username.contains(text, ignoreCase = true)
|
||||
|| node.host.remark.contains(text, ignoreCase = true)
|
||||
}
|
||||
|
||||
override fun canFilter(): Boolean {
|
||||
return searchTextField.text.isNotBlank()
|
||||
return searchTextField.text.trim().isNotBlank()
|
||||
}
|
||||
|
||||
})
|
||||
@@ -264,4 +266,4 @@ class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProv
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.termora.account
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.OptionsPane.Companion.FORM_MARGIN
|
||||
import app.termora.actions.AnAction
|
||||
import app.termora.actions.AnActionEvent
|
||||
@@ -8,21 +9,36 @@ import app.termora.database.DatabaseManager
|
||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import com.sun.net.httpserver.HttpServer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
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.jdesktop.swingx.JXBusyLabel
|
||||
import org.jdesktop.swingx.JXHyperlink
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.URI
|
||||
import java.net.URLEncoder
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.swing.*
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
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 databaseManager get() = DatabaseManager.getInstance()
|
||||
@@ -30,18 +46,31 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
|
||||
private val accountProperties get() = AccountProperties.getInstance()
|
||||
private val userInfoPanel = JPanel(BorderLayout())
|
||||
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 {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
private fun initView() {
|
||||
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() {
|
||||
// 服务器签名发生变更
|
||||
@@ -99,11 +128,7 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
|
||||
planBox.add(Box.createHorizontalStrut(16))
|
||||
val upgrade = JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.upgrade")) {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
if (I18n.isChinaMainland()) {
|
||||
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()}"))
|
||||
}
|
||||
Application.browse(URI.create("${accountManager.getServer()}/v1/client/redirect?to=upgrade&version=${Application.getVersion()}"))
|
||||
}
|
||||
})
|
||||
upgrade.isFocusable = false
|
||||
@@ -145,6 +170,29 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
|
||||
.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 {
|
||||
val actionBox = Box.createHorizontalBox()
|
||||
actionBox.add(Box.createHorizontalGlue())
|
||||
@@ -219,11 +267,139 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
|
||||
return actionBox
|
||||
}
|
||||
|
||||
private fun showLoginPanel() {
|
||||
refreshLoginPanel()
|
||||
busyLabel.isBusy = true
|
||||
cardLayout.show(contentPanel, "Login")
|
||||
}
|
||||
|
||||
private fun onLogin() {
|
||||
httpServer?.stop(0)
|
||||
|
||||
val dialog = LoginServerDialog(owner)
|
||||
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() {
|
||||
userInfoPanel.removeAll()
|
||||
userInfoPanel.add(getCenterComponent(), BorderLayout.CENTER)
|
||||
@@ -231,6 +407,13 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
|
||||
userInfoPanel.repaint()
|
||||
}
|
||||
|
||||
private fun refreshLoginPanel() {
|
||||
loginPanel.removeAll()
|
||||
loginPanel.add(getLoginComponent(), BorderLayout.CENTER)
|
||||
loginPanel.revalidate()
|
||||
loginPanel.repaint()
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.user
|
||||
}
|
||||
|
||||
@@ -9,12 +9,6 @@ import app.termora.database.DatabaseManager
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
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.jdesktop.swingx.JXHyperlink
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -24,12 +18,10 @@ import java.awt.Window
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
import java.net.URI
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.swing.*
|
||||
import javax.swing.event.ListDataEvent
|
||||
import javax.swing.event.ListDataListener
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
||||
companion object {
|
||||
@@ -37,18 +29,14 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
||||
}
|
||||
|
||||
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 cancelAction = super.createCancelAction()
|
||||
private val cancelButton = super.createJButtonForAction(cancelAction)
|
||||
private val isLoggingIn = AtomicBoolean(false)
|
||||
private val singaporeServer =
|
||||
Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app")
|
||||
private val chinaServer =
|
||||
Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn")
|
||||
private val serverManager get() = ServerManager.getInstance()
|
||||
var server: Server? = null
|
||||
|
||||
init {
|
||||
isModal = true
|
||||
@@ -60,12 +48,10 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
||||
size = Dimension(max(preferredSize.width, UIManager.getInt("Dialog.width") - 250), preferredSize.height)
|
||||
setLocationRelativeTo(owner)
|
||||
|
||||
passwordField.putClientProperty(FlatClientProperties.STYLE, mapOf("showCapsLock" to true))
|
||||
|
||||
addWindowListener(object : WindowAdapter() {
|
||||
override fun windowOpened(e: WindowEvent) {
|
||||
removeWindowListener(this)
|
||||
usernameTextField.requestFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -73,7 +59,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"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
|
||||
@@ -90,7 +76,6 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
||||
serverComboBox.addItem(Server(server.name, server.server))
|
||||
}
|
||||
|
||||
mfaTextField.placeholderText = I18n.getString("termora.settings.account.mfa")
|
||||
|
||||
serverComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
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() {
|
||||
if (serverComboBox.selectedItem == singaporeServer || serverComboBox.selectedItem == chinaServer || serverComboBox.itemCount < 1) {
|
||||
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")
|
||||
.add("${I18n.getString("termora.settings.account.server")}:").xy(1, rows)
|
||||
.add(serverComboBox).xy(3, rows)
|
||||
.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()
|
||||
}
|
||||
|
||||
@@ -315,95 +256,21 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
if (isLoggingIn.get()) return
|
||||
|
||||
val server = serverComboBox.selectedItem as? Server
|
||||
server = serverComboBox.selectedItem as? Server
|
||||
if (server == null) {
|
||||
serverComboBox.outline = FlatClientProperties.OUTLINE_ERROR
|
||||
serverComboBox.requestFocusInWindow()
|
||||
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()
|
||||
}
|
||||
|
||||
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() {
|
||||
if (isLoggingIn.get()) return
|
||||
server = null
|
||||
super.doCancelAction()
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,6 @@ class PullService private constructor() : SyncService(), Disposable, Application
|
||||
private var lastChangeHash = StringUtils.EMPTY
|
||||
|
||||
private fun pullChanges() {
|
||||
if (isFreePlan) return
|
||||
val hash: String
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package app.termora.account
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.AES
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.PBKDF2
|
||||
import app.termora.RSA
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
@@ -9,7 +12,6 @@ import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.apache.commons.codec.binary.Base64
|
||||
import org.apache.commons.codec.digest.DigestUtils
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
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()) {
|
||||
throw IllegalStateException("Already logged in")
|
||||
@@ -39,25 +41,25 @@ class ServerManager private constructor() {
|
||||
}
|
||||
|
||||
try {
|
||||
doLogin(server, username, password, mfa)
|
||||
doLogin(server, refreshToken, password)
|
||||
} finally {
|
||||
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)
|
||||
|
||||
// call login
|
||||
val loginResponse = callLogin(serverInfo, server, username, password, mfa)
|
||||
val loginResponse = callToken(server, refreshToken)
|
||||
|
||||
// call me
|
||||
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 privateKeySecureIv = PBKDF2.hash(salt, password.toCharArray(), 1024, 128)
|
||||
val privateKeyEncoded = AES.CBC.decrypt(
|
||||
@@ -106,29 +108,19 @@ class ServerManager private constructor() {
|
||||
return ohMyJson.decodeFromString<ServerInfo>(AccountHttp.execute(request = request))
|
||||
}
|
||||
|
||||
private fun callLogin(
|
||||
serverInfo: ServerInfo,
|
||||
private fun callToken(
|
||||
server: Server,
|
||||
username: String,
|
||||
password: String,
|
||||
mfa: String
|
||||
refreshToken: String,
|
||||
): LoginResponse {
|
||||
|
||||
val passwordHex = DigestUtils.sha256Hex("${serverInfo.salt}:${username}:${password}")
|
||||
val requestBody = ohMyJson.encodeToString(mapOf("email" to username, "password" to passwordHex, "mfa" to mfa))
|
||||
val body = ohMyJson.encodeToString(mapOf("refreshToken" to refreshToken))
|
||||
.toRequestBody("application/json".toMediaType())
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("${server.server}/v1/login")
|
||||
.post(requestBody)
|
||||
val request = Request.Builder().url("${server.server}/v1/token")
|
||||
.header("Authorization", "Bearer $refreshToken")
|
||||
.post(body)
|
||||
.build()
|
||||
|
||||
val response = AccountHttp.client.newCall(request).execute()
|
||||
val text = response.use { response.body.use { it?.string() } }
|
||||
|
||||
if (text == null) {
|
||||
throw ResponseException(response.code, response)
|
||||
}
|
||||
val text = response.use { response.body.use { it.string() } }
|
||||
|
||||
if (response.isSuccessful.not()) {
|
||||
val message = ohMyJson.parseToJsonElement(text).jsonObject["message"]?.jsonPrimitive?.content
|
||||
|
||||
@@ -287,6 +287,9 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
|
||||
|
||||
typeComboBox.addItem("RSA")
|
||||
typeComboBox.addItem("ED25519")
|
||||
typeComboBox.addItem("ECDSA-SHA2-NISTP256")
|
||||
typeComboBox.addItem("ECDSA-SHA2-NISTP384")
|
||||
typeComboBox.addItem("ECDSA-SHA2-NISTP521")
|
||||
|
||||
// 默认 RSA
|
||||
lengthComboBox.addItem(1024)
|
||||
@@ -396,6 +399,12 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
|
||||
lengthComboBox.addItem(1024 * 4)
|
||||
lengthComboBox.addItem(1024 * 8)
|
||||
lengthComboBox.selectedItem = 1024 * 2
|
||||
} else if (typeComboBox.selectedItem == "ECDSA-SHA2-NISTP256") {
|
||||
lengthComboBox.addItem(256)
|
||||
} else if (typeComboBox.selectedItem == "ECDSA-SHA2-NISTP384") {
|
||||
lengthComboBox.addItem(384)
|
||||
} else if (typeComboBox.selectedItem == "ECDSA-SHA2-NISTP521") {
|
||||
lengthComboBox.addItem(521)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,6 +422,17 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
|
||||
super.doCancelAction()
|
||||
}
|
||||
|
||||
private fun genKeyPair(): KeyPair {
|
||||
val keyType = when (typeComboBox.selectedItem) {
|
||||
"ED25519" -> KeyPairProvider.SSH_ED25519
|
||||
"ECDSA-SHA2-NISTP256" -> KeyPairProvider.ECDSA_SHA2_NISTP256
|
||||
"ECDSA-SHA2-NISTP384" -> KeyPairProvider.ECDSA_SHA2_NISTP384
|
||||
"ECDSA-SHA2-NISTP521" -> KeyPairProvider.ECDSA_SHA2_NISTP521
|
||||
else -> KeyPairProvider.SSH_RSA
|
||||
}
|
||||
return KeyUtils.generateKeyPair(keyType, lengthComboBox.selectedItem as Int)
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
|
||||
if (ohKeyPair == OhKeyPair.empty) {
|
||||
@@ -422,9 +442,7 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
|
||||
return
|
||||
}
|
||||
|
||||
val keyType = if (typeComboBox.selectedItem == "RSA")
|
||||
KeyPairProvider.SSH_RSA else KeyPairProvider.SSH_ED25519
|
||||
val keyPair = KeyUtils.generateKeyPair(keyType, lengthComboBox.selectedItem as Int)
|
||||
val keyPair = genKeyPair()
|
||||
ohKeyPair = OhKeyPair(
|
||||
id = randomUUID(),
|
||||
name = nameTextField.text,
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.termora.keymgr
|
||||
|
||||
import app.termora.AES.decodeBase64
|
||||
import app.termora.RSA
|
||||
import org.apache.sshd.common.config.keys.impl.ECDSAPublicKeyEntryDecoder
|
||||
import org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider
|
||||
import org.apache.sshd.common.session.SessionContext
|
||||
import org.apache.sshd.common.util.security.eddsa.Ed25519PublicKeyDecoder
|
||||
@@ -25,6 +26,8 @@ class OhKeyPairKeyPairProvider(private val id: String) : AbstractResourceKeyPair
|
||||
when (ohKeyPair.type) {
|
||||
"RSA" -> RSA.generatePublic(ohKeyPair.publicKey.decodeBase64())
|
||||
"ED25519" -> Ed25519PublicKeyDecoder.INSTANCE.generatePublicKey((X509EncodedKeySpec(ohKeyPair.publicKey.decodeBase64())))
|
||||
"ECDSA-SHA2-NISTP256","ECDSA-SHA2-NISTP384","ECDSA-SHA2-NISTP521" ->
|
||||
ECDSAPublicKeyEntryDecoder.INSTANCE.generatePublicKey(X509EncodedKeySpec(ohKeyPair.publicKey.decodeBase64()))
|
||||
else -> throw UnsupportedOperationException("${ohKeyPair.type} is not supported")
|
||||
}
|
||||
} as PublicKey
|
||||
@@ -33,6 +36,8 @@ class OhKeyPairKeyPairProvider(private val id: String) : AbstractResourceKeyPair
|
||||
when (ohKeyPair.type) {
|
||||
"RSA" -> RSA.generatePrivate(ohKeyPair.privateKey.decodeBase64())
|
||||
"ED25519" -> Ed25519PublicKeyDecoder.INSTANCE.generatePrivateKey(PKCS8EncodedKeySpec(ohKeyPair.privateKey.decodeBase64()))
|
||||
"ECDSA-SHA2-NISTP256","ECDSA-SHA2-NISTP384","ECDSA-SHA2-NISTP521" ->
|
||||
ECDSAPublicKeyEntryDecoder.INSTANCE.generatePrivateKey(PKCS8EncodedKeySpec(ohKeyPair.privateKey.decodeBase64()))
|
||||
else -> throw UnsupportedOperationException("${ohKeyPair.type} is not supported")
|
||||
}
|
||||
} as PrivateKey
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package app.termora.terminal
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
||||
|
||||
private var startPosition = Position.unknown
|
||||
private var endPosition = Position.unknown
|
||||
private var block = false
|
||||
private val document = terminal.getDocument()
|
||||
|
||||
internal companion object {
|
||||
private val log = LoggerFactory.getLogger(SelectionModelImpl::class.java)
|
||||
|
||||
fun isPointInsideArea(start: Position, end: Position, x: Int, y: Int, cols: Int): Boolean {
|
||||
val top = min(start.y, end.y)
|
||||
val bottom = max(start.y, end.y)
|
||||
@@ -49,7 +53,13 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
|
||||
}
|
||||
|
||||
// 设置新的选择区域
|
||||
setSelection(startPosition, endPosition)
|
||||
try {
|
||||
setSelection(startPosition, endPosition)
|
||||
} catch (e: Exception) {
|
||||
if (log.isTraceEnabled) {
|
||||
log.trace(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -146,6 +146,26 @@ class NewHostTree : SimpleTree(), Disposable {
|
||||
}
|
||||
})
|
||||
|
||||
// 开启 ToolTip 功能
|
||||
ToolTipManager.sharedInstance().registerComponent(this)
|
||||
|
||||
// 设置鼠标移动提示
|
||||
addMouseMotionListener(object : java.awt.event.MouseMotionAdapter() {
|
||||
override fun mouseMoved(e: MouseEvent) {
|
||||
val path: TreePath? = getPathForLocation(e.x, e.y)
|
||||
if (path != null) {
|
||||
val node: HostTreeNode = path.lastPathComponent as HostTreeNode
|
||||
if (node.host.remark.isNotEmpty()){
|
||||
toolTipText = node.host.remark
|
||||
}else{
|
||||
toolTipText = null
|
||||
}
|
||||
} else {
|
||||
toolTipText = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
actionMap.put("copy", object : AnAction() {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
toolkit.systemClipboard.setContents(StringSelection(StringUtils.EMPTY), null)
|
||||
@@ -193,6 +213,9 @@ class NewHostTree : SimpleTree(), Disposable {
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
// 销毁
|
||||
ToolTipManager.sharedInstance().unregisterComponent(this)
|
||||
|
||||
val name = super.getName()
|
||||
if (name.isNullOrBlank().not()) {
|
||||
properties.putString("${name}.state", TreeUtils.saveExpansionState(this))
|
||||
@@ -1151,4 +1174,4 @@ class NewHostTree : SimpleTree(), Disposable {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.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.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.lifetime=Lifetime
|
||||
termora.settings.account.upgrade=Upgrade
|
||||
|
||||
@@ -49,7 +49,26 @@ termora.setting.security.enter-password-again=Повторите пароль
|
||||
termora.setting.security.password-is-different=Пароли отличаются
|
||||
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=Терминал
|
||||
|
||||
@@ -103,10 +103,8 @@ termora.settings.plugin.not-compatible=插件 <b>{0}</b> 与当前版本不兼
|
||||
|
||||
termora.settings.account=账号
|
||||
termora.settings.account.login=登录
|
||||
termora.settings.account.register=注册
|
||||
termora.settings.account.not-support-register=该服务器不支持注册账号
|
||||
termora.settings.account.server=服务器
|
||||
termora.settings.account.mfa=多因素验证是可选的
|
||||
termora.settings.account.wait-login=正在等待默认浏览器中登录...
|
||||
termora.settings.account.locally=本地的
|
||||
termora.settings.account.lifetime=长期
|
||||
termora.settings.account.verify=验证
|
||||
|
||||
@@ -114,10 +114,8 @@ termora.settings.plugin.not-compatible=插件 <b>{0}</b> 與目前版本不相
|
||||
|
||||
termora.settings.account=帳號
|
||||
termora.settings.account.login=登入
|
||||
termora.settings.account.register=註冊
|
||||
termora.settings.account.not-support-register=此伺服器不支援註冊帳號
|
||||
termora.settings.account.server=伺服器
|
||||
termora.settings.account.mfa=多因素驗證是可選的
|
||||
termora.settings.account.wait-login=正在等待預設瀏覽器登入...
|
||||
termora.settings.account.locally=本地的
|
||||
termora.settings.account.lifetime=長期
|
||||
termora.settings.account.verify=驗證
|
||||
|
||||
@@ -15,6 +15,19 @@ class KeyUtilsTest {
|
||||
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair("ssh-rsa", 1024).public), 1024)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun test_ECDSA() {
|
||||
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP256, 256).private), 256)
|
||||
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP256, 256).public), 256)
|
||||
|
||||
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP384, 384).private), 384)
|
||||
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP384, 384).public), 384)
|
||||
|
||||
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP521, 521).private), 521)
|
||||
assertEquals(KeyUtils.getKeySize(KeyUtils.generateKeyPair(KeyPairProvider.ECDSA_SHA2_NISTP521, 521).public), 521)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_ed25519() {
|
||||
val keyPair = KeyUtils.generateKeyPair(KeyPairProvider.SSH_ED25519, 256)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM linuxserver/openssh-server
|
||||
FROM linuxserver/openssh-server:9.3_p2-r1-ls147
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||
&& apk update && apk add wget tmux gcc zip p7zip g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
|
||||
&& tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \
|
||||
|
||||
Reference in New Issue
Block a user