Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
683c333955 chore(deps): bump org.apache.sshd:sshd-core from 2.15.0 to 2.16.0
Bumps [org.apache.sshd:sshd-core](https://github.com/apache/mina-sshd) from 2.15.0 to 2.16.0.
- [Release notes](https://github.com/apache/mina-sshd/releases)
- [Changelog](https://github.com/apache/mina-sshd/blob/master/CHANGES.md)
- [Commits](https://github.com/apache/mina-sshd/compare/sshd-2.15.0...sshd-2.16.0)

---
updated-dependencies:
- dependency-name: org.apache.sshd:sshd-core
  dependency-version: 2.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-25 04:34:58 +00:00
74 changed files with 371 additions and 785 deletions

View File

@@ -3,8 +3,8 @@ name: Linux
on: [ push, pull_request ]
env:
JBR_MAJOR: 21.0.8
JBR_PATCH: b1163.69
JBR_MAJOR: 21.0.7
JBR_PATCH: b1038.58
jobs:
build:

View File

@@ -8,15 +8,15 @@ 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.8
JBR_PATCH: b1163.69
JBR_MAJOR: 21.0.7
JBR_PATCH: b1038.58
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ macos-15-intel, macos-latest ]
os: [ macos-15, macos-13 ]
steps:
- uses: actions/checkout@v4
with:

View File

@@ -3,8 +3,8 @@ name: Windows
on: [ push, pull_request ]
env:
JBR_MAJOR: 21.0.8
JBR_PATCH: b1163.69
JBR_MAJOR: 21.0.7
JBR_PATCH: b1038.58
jobs:
build:

View File

@@ -1 +1 @@
2.0.0-beta.15
2.0.0-beta.13

View File

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

View File

@@ -1,50 +1,50 @@
[versions]
kotlin = "2.3.0"
kotlin = "2.2.10"
slf4j = "2.0.17"
pty4j = "0.13.10"
tinylog = "2.7.0"
kotlinx-coroutines = "1.10.2"
flatlaf = "3.7"
flatlaf = "3.6.1"
kotlinx-serialization-json = "1.9.0"
commons-codec = "1.20.0"
commons-lang3 = "3.20.0"
commons-codec = "1.19.0"
commons-lang3 = "3.18.0"
commons-csv = "1.14.1"
commons-net = "3.12.0"
commons-text = "1.15.0"
commons-text = "1.14.0"
commons-compress = "1.28.0"
commons-vfs2 = "2.10.0"
swingx = "1.6.5-1"
jgoodies-forms = "1.9.0"
jfa = "1.2.0"
oshi = "6.9.1"
oshi = "6.8.1"
versioncompare = "1.4.1"
jna = "5.18.1"
jna = "5.17.0"
jSystemThemeDetector = "3.9.1"
commons-io = "2.21.0"
commons-io = "2.20.0"
jbr-api = "17.1.10.1"
hutool = "5.8.40"
hutool = "5.8.39"
jsch = "2.27.3"
okhttp = "5.3.0"
okhttp = "5.1.0"
sshj = "0.39.0"
sshd-core = "2.15.0"
jgit = "7.4.0.202509020913-r"
commonmark = "0.27.0"
sshd-core = "2.16.0"
jgit = "7.2.0.202503040940-r"
commonmark = "0.25.1"
jnafilechooser = "1.1.2"
xodus = "2.0.1"
bip39 = "1.0.9"
colorpicker = "2.0.1"
rhino = "1.9.0"
delight-rhino-sandbox = "0.2.1"
testcontainers = "2.0.3"
mixpanel = "1.7.0"
jSerialComm = "2.11.4"
rhino = "1.8.0"
delight-rhino-sandbox = "0.0.17"
testcontainers = "1.21.3"
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-4"
exposed = "1.0.0-beta-5"
h2 = "2.3.232"
sqlite = "3.50.3.0"
jug = "5.2.0"
jug = "5.1.0"
semver4j = "6.0.0"
jsvg = "2.0.0"
dom4j = "2.2.0"
@@ -70,7 +70,7 @@ flatlafextras = { group = "com.formdev", name = "flatlaf-extras", version.ref =
flatlafswingx = { module = "com.formdev:flatlaf-swingx", version.ref = "flatlaf" }
testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" }
testcontainers = { module = "org.testcontainers:testcontainers" }
testcontainers-junit-jupiter = { module = "org.testcontainers:testcontainers-junit-jupiter" }
testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter" }
swingx = { module = "org.swinglabs.swingx:swingx-all", version.ref = "swingx" }
jgoodies-forms = { module = "com.jgoodies:jgoodies-forms", version.ref = "jgoodies-forms" }
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
@@ -106,7 +106,7 @@ eddsa = { module = "net.i2p.crypto:eddsa", version.ref = "eddsa" }
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
exposed-crypt = { module = "org.jetbrains.exposed:exposed-crypt", version.ref = "exposed" }
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
exposed-migration = { module = "org.jetbrains.exposed:exposed-migration-core", version.ref = "exposed" }
exposed-migration = { module = "org.jetbrains.exposed:exposed-migration", version.ref = "exposed" }
h2 = { module = "com.h2database:h2", version.ref = "h2" }
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
jug = { module = "com.fasterxml.uuid:java-uuid-generator", version.ref = "jug" }

View File

@@ -8,7 +8,7 @@ project.version = "0.0.4"
dependencies {
testImplementation(kotlin("test"))
implementation("com.qcloud:cos_api:5.6.260.1")
implementation("com.qcloud:cos_api:5.6.251")
compileOnly(project(":"))
}

View File

@@ -4,13 +4,13 @@ plugins {
project.version = "0.0.8"
project.version = "0.0.7"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation("com.fifesoft:rsyntaxtextarea:3.6.1")
implementation("com.fifesoft:rsyntaxtextarea:3.6.0")
implementation("com.fifesoft:languagesupport:3.4.0")
implementation("com.fifesoft:autocomplete:3.3.2")
}

View File

@@ -1,6 +1,9 @@
package app.termora.plugins.editor
import app.termora.*
import app.termora.DocumentAdaptor
import app.termora.DynamicColor
import app.termora.EnableManager
import app.termora.Icons
import app.termora.database.DatabaseManager
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.components.FlatTextField
@@ -36,10 +39,6 @@ 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)
@@ -55,7 +54,6 @@ 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)
@@ -143,18 +141,11 @@ 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)
@@ -220,8 +211,6 @@ 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()

View File

@@ -1,6 +1,2 @@
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

View File

@@ -1,6 +1 @@
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=Формат
termora.plugins.editor.not-save=Файл не сохранён. Вы уверены, что хотите выйти?

View File

@@ -1,6 +1 @@
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=格式化

View File

@@ -1,6 +1 @@
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=格式化
termora.plugins.editor.not-save=檔案尚未儲存,你確定要退出嗎?

View File

@@ -1,4 +0,0 @@
<!-- 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>

Before

Width:  |  Height:  |  Size: 357 B

View File

@@ -1,4 +0,0 @@
<!-- 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>

Before

Width:  |  Height:  |  Size: 357 B

View File

@@ -7,7 +7,7 @@ project.version = "0.0.2"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation("org.apache.commons:commons-pool2:2.13.0")
implementation("org.apache.commons:commons-pool2:2.12.1")
testImplementation(project(":"))
}

View File

@@ -7,9 +7,9 @@ project.version = "0.0.8"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation("com.maxmind.geoip2:geoip2:5.0.0")
implementation("com.maxmind.geoip2:geoip2:4.3.1")
// https://github.com/hstyi/geolite2
implementation("com.github.hstyi:geolite2:v1.0-202510270056")
implementation("com.github.hstyi:geolite2:v1.0-202508110059")
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -13,7 +13,7 @@ dependencies {
testImplementation(libs.testcontainers.junit.jupiter)
testImplementation(project(":"))
implementation("io.minio:minio:8.6.0")
implementation("io.minio:minio:8.5.17")
compileOnly(project(":"))
}

View File

@@ -10,7 +10,7 @@ project.version = "0.0.5"
dependencies {
testImplementation(kotlin("test"))
compileOnly(project(":"))
implementation("com.fazecast:jSerialComm:2.11.4")
implementation("com.fazecast:jSerialComm:2.11.2")
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -2,7 +2,7 @@ plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.4"
project.version = "0.0.3"
dependencies {
testImplementation(kotlin("test"))

View File

@@ -42,7 +42,6 @@ class SMBHostOptionsPane : OptionsPane() {
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
extras = mutableMapOf(
"smb.share" to generalOption.shareTextField.text,
"smb.domain" to generalOption.domainTextField.text,
)
)
@@ -67,7 +66,6 @@ class SMBHostOptionsPane : OptionsPane() {
generalOption.remarkTextArea.text = host.remark
generalOption.passwordTextField.text = host.authentication.password
generalOption.shareTextField.text = host.options.extras["smb.share"] ?: StringUtils.EMPTY
generalOption.domainTextField.text = host.options.extras["smb.domain"] ?: StringUtils.EMPTY
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
}
@@ -116,7 +114,6 @@ class SMBHostOptionsPane : OptionsPane() {
val nameTextField = OutlineTextField(128)
val shareTextField = OutlineTextField(256)
val usernameTextField = OutlineComboBox<String>()
val domainTextField = OutlineTextField(128)
val hostTextField = OutlineTextField(255)
val passwordTextField = OutlinePasswordField(255)
val remarkTextArea = FixedLengthTextArea(512)
@@ -191,9 +188,7 @@ class SMBHostOptionsPane : OptionsPane() {
.add(portTextField).xy(7, rows).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
.add(usernameTextField).xy(3, rows)
.add("${SMBI18n.getString("termora.plugins.smb.domain")}:").xy(5, rows)
.add(domainTextField).xy(7, rows).apply { rows += step }
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }

View File

@@ -30,7 +30,6 @@ class SMBProtocolProvider private constructor() : TransferProtocolProvider {
val client = SMBClient()
val host = requester.host
val connection = client.connect(host.host, host.port)
val domain = host.options.extras["smb.domain"] ?: StringUtils.EMPTY
val session = when (host.username) {
"Guest" -> connection.authenticate(AuthenticationContext.guest())
"Anonymous" -> connection.authenticate(AuthenticationContext.anonymous())
@@ -38,7 +37,7 @@ class SMBProtocolProvider private constructor() : TransferProtocolProvider {
AuthenticationContext(
host.username,
host.authentication.password.toCharArray(),
domain.ifBlank { null }
null
)
)
}

View File

@@ -1,2 +1 @@
termora.plugins.smb.share=Share name
termora.plugins.smb.domain=Domain

View File

@@ -1,3 +1 @@
termora.plugins.smb.share=共享名称
termora.plugins.smb.domain=域名

View File

@@ -1,3 +1 @@
termora.plugins.smb.share=共享名稱
termora.plugins.smb.domain=網域

View File

@@ -50,17 +50,6 @@ class ApplicationInitializr {
}
}
// https://github.com/TermoraDev/termora/issues/1254
if (System.getProperty(FlatSystemProperties.UI_SCALE).isNullOrBlank()) {
val scale = System.getenv("TERMORA_SCALE")
if (scale.isNullOrBlank().not()) {
if (NumberUtils.toDouble(scale, -1.0) > 0) {
System.setProperty(FlatSystemProperties.UI_SCALE_ENABLED, "true")
System.setProperty(FlatSystemProperties.UI_SCALE, scale)
}
}
}
// 启动
val runtime = measureTimeMillis { ApplicationRunner().run() }
val log = LoggerFactory.getLogger(javaClass)

View File

@@ -10,11 +10,15 @@ import com.formdev.flatlaf.extras.FlatInspector
import com.formdev.flatlaf.ui.FlatTableCellBorder
import com.formdev.flatlaf.util.SystemInfo
import com.jthemedetecor.OsThemeDetector
import com.mixpanel.mixpanelapi.ClientDelivery
import com.mixpanel.mixpanelapi.MessageBuilder
import com.mixpanel.mixpanelapi.MixpanelAPI
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.LocaleUtils
import org.apache.commons.lang3.SystemUtils
import org.json.JSONObject
import org.slf4j.LoggerFactory
import java.awt.*
import java.awt.desktop.AppReopenedEvent
@@ -365,8 +369,61 @@ class ApplicationRunner {
if (Application.isUnknownVersion()) {
return
}
MixpanelService.getInstance().push("launch")
swingCoroutineScope.launch(Dispatchers.IO) {
try {
val properties = JSONObject()
properties.put("os", SystemUtils.OS_NAME)
if (SystemInfo.isLinux) {
properties.put("platform", "Linux")
} else if (SystemInfo.isWindows) {
properties.put("platform", "Windows")
} else if (SystemInfo.isMacOS) {
properties.put("platform", "macOS")
}
properties.put("version", Application.getVersion())
properties.put("language", Locale.getDefault().toString())
val message = MessageBuilder("0871335f59ee6d0eb246b008a20f9d1c")
.event(getAnalyticsUserID(), "launch", properties)
val delivery = ClientDelivery()
delivery.addMessage(message)
val endpoints = listOf(
"https://api-eu.mixpanel.com",
"https://api-in.mixpanel.com",
"https://api.mixpanel.com",
"http://api.mixpanel.com",
)
for (endpoint in endpoints) {
try {
MixpanelAPI(
"$endpoint/track",
"$endpoint/engage",
"$endpoint/groups"
).deliver(delivery, true)
break
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
continue
}
}
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
}
}
private fun getAnalyticsUserID(): String {
val properties = DatabaseManager.getInstance().properties
var id = properties.getString("AnalyticsUserID")
if (id.isNullOrBlank()) {
id = randomUUID()
properties.putString("AnalyticsUserID", id)
}
return id
}
}

View File

@@ -874,8 +874,6 @@ class NordDarkLaf : FlatPropertiesLaf("Nord Dark", Properties().apply {
TerminalColor.Basic.SELECTION_BACKGROUND,
TerminalColor.Cursor.BACKGROUND -> 0xeceff4
TerminalColor.Basic.SELECTION_FOREGROUND -> 0x3b4252
TerminalColor.Basic.FOREGROUND -> 0xd8dee9

View File

@@ -1,85 +0,0 @@
package app.termora
import app.termora.database.DatabaseManager
import com.formdev.flatlaf.util.SystemInfo
import com.mixpanel.mixpanelapi.ClientDelivery
import com.mixpanel.mixpanelapi.MessageBuilder
import com.mixpanel.mixpanelapi.MixpanelAPI
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.apache.commons.lang3.SystemUtils
import org.json.JSONObject
import org.slf4j.LoggerFactory
import java.util.*
internal class MixpanelService private constructor() {
companion object {
private val log = LoggerFactory.getLogger(MixpanelService::class.java)
fun getInstance(): MixpanelService {
return ApplicationScope.forApplicationScope().getOrCreate(MixpanelService::class) { MixpanelService() }
}
}
fun push(event: String, extras: Map<String, String> = emptyMap()) {
swingCoroutineScope.launch(Dispatchers.IO) {
try {
val properties = JSONObject()
for (entry in extras) {
properties.put(entry.key, entry.value)
}
properties.put("os", SystemUtils.OS_NAME)
if (SystemInfo.isLinux) {
properties.put("platform", "Linux")
} else if (SystemInfo.isWindows) {
properties.put("platform", "Windows")
} else if (SystemInfo.isMacOS) {
properties.put("platform", "macOS")
}
properties.put("version", Application.getVersion())
properties.put("language", Locale.getDefault().toString())
val message = MessageBuilder("0871335f59ee6d0eb246b008a20f9d1c")
.event(getAnalyticsUserID(), event, properties)
val delivery = ClientDelivery()
delivery.addMessage(message)
val endpoints = listOf(
"https://api-eu.mixpanel.com",
"https://api-in.mixpanel.com",
"https://api.mixpanel.com",
"http://api.mixpanel.com",
)
for (endpoint in endpoints) {
try {
MixpanelAPI(
"$endpoint/track",
"$endpoint/engage",
"$endpoint/groups"
).deliver(delivery, true)
break
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
continue
}
}
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
}
}
private fun getAnalyticsUserID(): String {
val properties = DatabaseManager.getInstance().properties
var id = properties.getString("AnalyticsUserID")
if (id.isNullOrBlank()) {
id = randomUUID()
properties.putString("AnalyticsUserID", id)
}
return id
}
}

View File

@@ -3,5 +3,5 @@ package app.termora
import app.termora.actions.AnActionEvent
import java.util.*
class OpenHostActionEvent(source: Any, val host: Host, event: EventObject, val tabIndex: Int = -1) :
class OpenHostActionEvent(source: Any, val host: Host, event: EventObject) :
AnActionEvent(source, String(), event)

View File

@@ -179,7 +179,7 @@ abstract class PtyHostTerminalTab(
val tab = createReconnectTerminalTab()
manager.addTerminalTab(index, tab, true)
manager.closeTerminalTab(this, disposable = true, reconnect = true)
manager.closeTerminalTab(this, true)
if (tab is HostTerminalTab) {
tab.start()

View File

@@ -547,7 +547,6 @@ class SettingsOptionsPane : OptionsPane() {
rightClickComboBox.addItem("Copy")
rightClickComboBox.addItem("CopyAndPaste")
rightClickComboBox.addItem("Nothing")
rightClickComboBox.selectedItem = terminalSetting.rightClick
@@ -577,8 +576,6 @@ class SettingsOptionsPane : OptionsPane() {
text = I18n.getString("termora.settings.terminal.right-click.copy")
} else if (value == "CopyAndPaste") {
text = I18n.getString("termora.settings.terminal.right-click.copy-and-paste")
}else if (value == "Nothing") {
text = I18n.getString("termora.settings.terminal.right-click.nothing")
}
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
}

View File

@@ -141,28 +141,25 @@ class TerminalTabbed(
}
private fun removeTabAt(index: Int, disposable: Boolean = true, reconnect: Boolean = false) {
private fun removeTabAt(index: Int, disposable: Boolean = true) {
if (tabbedPane.isTabClosable(index)) {
val tab = tabs[index]
// 询问是否可以关闭
if (disposable) {
// 如果是重连接,那么直接关闭不进行任何形式的询问
if (reconnect.not()) {
// 如果开启了关闭确认,那么直接询问用户
if (appearance.confirmTabClose) {
if (OptionPane.showConfirmDialog(
windowScope.window,
I18n.getString("termora.tabbed.tab.close-prompt"),
messageType = JOptionPane.QUESTION_MESSAGE,
optionType = JOptionPane.OK_CANCEL_OPTION
) != JOptionPane.OK_OPTION
) {
return
}
} else if (!tab.willBeClose()) { // 如果没有开启则询问用户
// 如果开启了关闭确认,那么直接询问用户
if (appearance.confirmTabClose) {
if (OptionPane.showConfirmDialog(
windowScope.window,
I18n.getString("termora.tabbed.tab.close-prompt"),
messageType = JOptionPane.QUESTION_MESSAGE,
optionType = JOptionPane.OK_CANCEL_OPTION
) != JOptionPane.OK_OPTION
) {
return
}
} else if (!tab.willBeClose()) { // 如果没有开启则询问用户
return
}
}
@@ -236,7 +233,7 @@ class TerminalTabbed(
if (tab is HostTerminalTab) {
actionManager
.getAction(OpenHostAction.OPEN_HOST)
.actionPerformed(OpenHostActionEvent(this, tab.host, evt, tabIndex + 1))
.actionPerformed(OpenHostActionEvent(this, tab.host, evt))
}
}
@@ -364,7 +361,7 @@ class TerminalTabbed(
}
}
override fun indexOfTerminalTab(tab: TerminalTab): Int {
override fun indexOfTerminalTab(tab: TerminalTab):Int {
return tabbedPane.indexOfComponent(tab.getJComponent())
}
@@ -454,10 +451,10 @@ class TerminalTabbed(
}
}
override fun closeTerminalTab(tab: TerminalTab, disposable: Boolean, reconnect: Boolean) {
override fun closeTerminalTab(tab: TerminalTab, disposable: Boolean) {
for (i in 0 until tabs.size) {
if (tabs[i] == tab) {
removeTabAt(i, disposable, reconnect)
removeTabAt(i, disposable)
break
}
}

View File

@@ -6,7 +6,7 @@ interface TerminalTabbedManager {
fun getSelectedTerminalTab(): TerminalTab?
fun getTerminalTabs(): List<TerminalTab>
fun setSelectedTerminalTab(tab: TerminalTab)
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true, reconnect: Boolean = false)
fun closeTerminalTab(tab: TerminalTab, disposable: Boolean = true)
fun refreshTerminalTabs()
fun indexOfTerminalTab(tab: TerminalTab): Int
}

View File

@@ -170,18 +170,16 @@ class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProv
filterableTreeModel.addFilter(object : Filter {
override fun filter(node: Any): Boolean {
val text = searchTextField.text.trim()
val text = searchTextField.text
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, ignoreCase = true)
|| node.host.host.contains(text, ignoreCase = true)
|| node.host.username.contains(text, ignoreCase = true)
|| node.host.remark.contains(text, ignoreCase = true)
return node.host.name.contains(text) || node.host.host.contains(text)
|| node.host.username.contains(text)
}
override fun canFilter(): Boolean {
return searchTextField.text.trim().isNotBlank()
return searchTextField.text.isNotBlank()
}
})
@@ -266,4 +264,4 @@ class WelcomePanel() : JPanel(BorderLayout()), Disposable, TerminalTab, DataProv
}
}
}

View File

@@ -1,7 +1,6 @@
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
@@ -9,36 +8,21 @@ 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()
@@ -46,32 +30,19 @@ 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()
refreshLoginPanel()
contentPanel.add(userInfoPanel, "UserInfo")
contentPanel.add(loginPanel, "Login")
cardLayout.show(contentPanel, "UserInfo")
add(contentPanel, BorderLayout.CENTER)
add(userInfoPanel, BorderLayout.CENTER)
}
private fun initEvents() {
// 服务器签名发生变更
DynamicExtensionHandler.getInstance()
@@ -128,7 +99,11 @@ 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) {
Application.browse(URI.create("${accountManager.getServer()}/v1/client/redirect?to=upgrade&version=${Application.getVersion()}"))
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()}"))
}
}
})
upgrade.isFocusable = false
@@ -170,29 +145,6 @@ 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())
@@ -267,139 +219,11 @@ 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)
@@ -407,13 +231,6 @@ 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
}

View File

@@ -9,6 +9,12 @@ 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
@@ -18,10 +24,12 @@ 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 {
@@ -29,14 +37,18 @@ 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")
var server: Server? = null
private val serverManager get() = ServerManager.getInstance()
init {
isModal = true
@@ -48,10 +60,12 @@ 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()
}
})
}
@@ -59,7 +73,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
@@ -76,6 +90,7 @@ 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(
@@ -138,6 +153,40 @@ 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")
@@ -165,11 +214,21 @@ 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()
}
@@ -256,21 +315,95 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
}
override fun doOKAction() {
if (isLoggingIn.get()) return
server = serverComboBox.selectedItem as? Server
val 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() {
server = null
if (isLoggingIn.get()) return
super.doCancelAction()
}
}

View File

@@ -67,11 +67,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
private var lastChangeHash = StringUtils.EMPTY
private fun pullChanges() {
if (accountManager.isLocally()) {
return
}
if (isFreePlan) return
val hash: String
try {

View File

@@ -1,10 +1,7 @@
package app.termora.account
import app.termora.AES
import app.termora.*
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
@@ -12,6 +9,7 @@ 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() {
@@ -30,7 +28,7 @@ class ServerManager private constructor() {
/**
* 登录,不报错就是登录成功
*/
fun login(server: Server, refreshToken: String, password: String) {
fun login(server: Server, username: String, password: String, mfa: String) {
if (accountManager.isLocally().not()) {
throw IllegalStateException("Already logged in")
@@ -41,25 +39,25 @@ class ServerManager private constructor() {
}
try {
doLogin(server, refreshToken, password)
doLogin(server, username, password, mfa)
} finally {
isLoggingIn.compareAndSet(true, false)
}
}
private fun doLogin(server: Server, refreshToken: String, password: String) {
private fun doLogin(server: Server, username: String, password: String, mfa: String) {
// 服务器信息
val serverInfo = getServerInfo(server)
// call login
val loginResponse = callToken(server, refreshToken)
val loginResponse = callLogin(serverInfo, server, username, password, mfa)
// call me
val meResponse = callMe(server.server, loginResponse.accessToken)
// 解密
val salt = "${serverInfo.salt}:${meResponse.email}".toByteArray()
val salt = "${serverInfo.salt}:${username}".toByteArray()
val privateKeySecureKey = PBKDF2.hash(salt, password.toCharArray(), 1024, 256)
val privateKeySecureIv = PBKDF2.hash(salt, password.toCharArray(), 1024, 128)
val privateKeyEncoded = AES.CBC.decrypt(
@@ -108,19 +106,29 @@ class ServerManager private constructor() {
return ohMyJson.decodeFromString<ServerInfo>(AccountHttp.execute(request = request))
}
private fun callToken(
private fun callLogin(
serverInfo: ServerInfo,
server: Server,
refreshToken: String,
username: String,
password: String,
mfa: String
): 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())
val request = Request.Builder().url("${server.server}/v1/token")
.header("Authorization", "Bearer $refreshToken")
.post(body)
val request = Request.Builder()
.url("${server.server}/v1/login")
.post(requestBody)
.build()
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()) {
val message = ohMyJson.parseToJsonElement(text).jsonObject["message"]?.jsonPrimitive?.content

View File

@@ -7,7 +7,6 @@ import org.apache.commons.codec.binary.Base64
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.lang3.ObjectUtils
import org.apache.commons.lang3.StringUtils
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.update

View File

@@ -56,12 +56,7 @@ class OpenHostAction : AnAction() {
if (tab == null) return
if (evt.tabIndex >= 0) {
terminalTabbedManager.addTerminalTab(evt.tabIndex, tab)
} else {
terminalTabbedManager.addTerminalTab(tab)
}
terminalTabbedManager.addTerminalTab(tab)
if (tab is PtyHostTerminalTab) {
tab.start()
}

View File

@@ -20,10 +20,6 @@ class TerminalCopyAction : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
val terminalPanel = evt.getData(DataProviders.TerminalPanel) ?: return
val selectionModel = terminalPanel.terminal.getSelectionModel()
if (!selectionModel.hasSelection()) {
return
}
val text = terminalPanel.copy()
val systemClipboard = terminalPanel.toolkit.systemClipboard
@@ -57,4 +53,4 @@ class TerminalCopyAction : AnAction() {
}
}
}

View File

@@ -16,8 +16,8 @@ import app.termora.snippet.SnippetManager
import app.termora.terminal.CursorStyle
import org.apache.commons.io.FileUtils
import org.apache.commons.lang3.StringUtils
import org.jetbrains.exposed.v1.core.SqlExpressionBuilder.eq
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.statements.StatementType
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager

View File

@@ -27,14 +27,12 @@ class KeyboardInteractiveDialog(
isModal = true
isResizable = true
controlsVisible = false
title = I18n.getString("termora.new-host.title")
init()
pack()
size = Dimension(max(300, size.width), size.height)
// fix https://github.com/TermoraDev/termora/issues/1311
pack()
setLocationRelativeTo(null)
}

View File

@@ -30,7 +30,6 @@ class TerminalUserInteraction(
)
dialog.setLocationRelativeTo(owner)
dialog.title = instruction ?: name ?: "OTP"
dialog.title = StringUtils.defaultIfBlank(dialog.title, "OTP")
passwords[i] = dialog.getText()
if (passwords[i].isBlank()) {
break

View File

@@ -29,10 +29,8 @@ class KeymapPanel : JPanel(BorderLayout()) {
private val copyBtn = JButton(Icons.copy)
private val renameBtn = JButton(Icons.edit)
private val deleteBtn = JButton(Icons.delete)
private val infoBtn = JButton(Icons.questionMark)
private val database get() = DatabaseManager.getInstance()
private val allowKeyCodes = mutableSetOf<Int>()
private val owner get() = SwingUtilities.getWindowAncestor(this)
init {
initView()
@@ -91,8 +89,8 @@ class KeymapPanel : JPanel(BorderLayout()) {
box.add(copyBtn)
box.add(renameBtn)
box.add(deleteBtn)
box.add(infoBtn)
box.add(Box.createHorizontalGlue())
box.border = BorderFactory.createEmptyBorder(0, 0, 6, 0)
add(box, BorderLayout.NORTH)
add(scrollPane, BorderLayout.CENTER)
@@ -107,12 +105,6 @@ class KeymapPanel : JPanel(BorderLayout()) {
}
})
infoBtn.addActionListener {
val color = UIManager.getColor("TextField.placeholderForeground")
val msg = I18n.getString("termora.settings.keymap.question", color.red, color.green, color.blue)
OptionPane.showMessageDialog(owner, msg)
}
copyBtn.addActionListener {
val keymap = getCurrentKeymap()
if (keymap != null) {

View File

@@ -287,9 +287,6 @@ 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)
@@ -399,12 +396,6 @@ 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)
}
}
}
@@ -422,17 +413,6 @@ 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) {
@@ -442,7 +422,9 @@ class KeyManagerPanel(private val accountOwner: AccountOwner) : JPanel(BorderLay
return
}
val keyPair = genKeyPair()
val keyType = if (typeComboBox.selectedItem == "RSA")
KeyPairProvider.SSH_RSA else KeyPairProvider.SSH_ED25519
val keyPair = KeyUtils.generateKeyPair(keyType, lengthComboBox.selectedItem as Int)
ohKeyPair = OhKeyPair(
id = randomUUID(),
name = nameTextField.text,

View File

@@ -2,7 +2,6 @@ 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
@@ -26,8 +25,6 @@ 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
@@ -36,8 +33,6 @@ 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

View File

@@ -32,7 +32,7 @@ class MacroManager private constructor() {
val accountId = AccountManager.getInstance().getAccountId()
database.saveAndIncrementVersion(
database.save(
Data(
id = macro.id,
ownerId = accountId,

View File

@@ -185,13 +185,6 @@ class PluginPanel(val descriptor: PluginPluginDescriptor) : JPanel(), Disposable
}
}
MixpanelService.getInstance().push(
"uninstall-plugin", mapOf(
"pluginName" to descriptor.plugin.getName(),
"pluginVersion" to descriptor.version.toString(),
)
)
// 询问是否重启
TermoraRestarter.getInstance().scheduleRestart(owner)
} else {
@@ -234,13 +227,6 @@ class PluginPanel(val descriptor: PluginPluginDescriptor) : JPanel(), Disposable
}
}, button == updateButton)
MixpanelService.getInstance().push(
"${if (button == installButton) "install" else "update"}-plugin", mapOf(
"pluginName" to descriptor.plugin.getName(),
"pluginVersion" to descriptor.version.toString(),
)
)
withContext(Dispatchers.Swing) {
installed.add(descriptor.id)

View File

@@ -10,8 +10,6 @@ import kotlinx.coroutines.launch
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.Strings
import org.apache.commons.lang3.SystemUtils
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
import java.net.URI
@@ -66,15 +64,7 @@ internal class RDPProtocolProvider private constructor() : GenericProtocolProvid
}
val sb = StringBuilder()
sb.append("full address:s:")
if (SystemUtils.IS_OS_WINDOWS && Strings.CI.contains(host.host, ":")) {
var newHost = Strings.CI.removeStart(host.host, "[")
newHost = Strings.CI.removeEnd(newHost, "]")
sb.append('[').append(newHost).append(']')
} else {
sb.append(host.host)
}
sb.append(':').append(host.port).appendLine()
sb.append("full address:s:").append(host.host).append(':').append(host.port).appendLine()
sb.append("username:s:").append(host.username).appendLine()
val desktop = host.options.extras["desktop"]
if (desktop.isNullOrBlank().not()) {

View File

@@ -27,14 +27,9 @@ class CloneSessionTerminalTabbedContextMenuExtension private constructor() : Ter
cloneSession.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
val index = terminalTabbedManager.indexOfTerminalTab(tab)
val handler = c.copy(channel = null)
val newTab = SSHTerminalTab(windowScope, tab.host, handler)
if (index >= 0) {
terminalTabbedManager.addTerminalTab(index + 1, newTab)
} else {
terminalTabbedManager.addTerminalTab(newTab)
}
terminalTabbedManager.addTerminalTab(newTab)
newTab.start()
}
})

View File

@@ -332,12 +332,6 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
var top = sr.getOrElse(0) { 1 }
var bottom = sr.getOrElse(1) { terminalModel.getRows() }
// ";r" https://vt100.net/docs/vt510-rm/DECSTBM.html
if (sr.size == 1 && args.startsWith(';')) {
bottom = top
top = 1
}
if (bottom <= top) {
if (log.isWarnEnabled) {
log.warn("Set Scrolling Region Error. top: $top , bottom: $bottom")

View File

@@ -1,20 +1,16 @@
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)
@@ -53,13 +49,7 @@ open class SelectionModelImpl(private val terminal: Terminal) : SelectionModel {
}
// 设置新的选择区域
try {
setSelection(startPosition, endPosition)
} catch (e: Exception) {
if (log.isTraceEnabled) {
log.trace(e.message)
}
}
setSelection(startPosition, endPosition)
}

View File

@@ -1,6 +1,5 @@
package app.termora.terminal.panel
import app.termora.actions.TerminalCopyAction
import app.termora.keymap.KeyShortcut
import app.termora.keymap.KeymapManager
import app.termora.plugin.internal.AltKeyModifier
@@ -72,7 +71,6 @@ class TerminalPanelKeyAdapter(
}
val keyStroke = KeyStroke.getKeyStrokeForEvent(e)
val keymapActions = activeKeymap.getActionIds(KeyShortcut(keyStroke))
for (action in terminalPanel.getTerminalActions()) {
if (action.test(keyStroke, e)) {
action.actionPerformed(e)
@@ -106,9 +104,7 @@ class TerminalPanelKeyAdapter(
}
// 如果命中了全局快捷键,那么不处理
val copyShortcutWithoutSelection =
keymapActions.contains(TerminalCopyAction.COPY) && terminal.getSelectionModel().hasSelection().not()
if (keyStroke.modifiers != 0 && keymapActions.isNotEmpty() && !copyShortcutWithoutSelection) {
if (keyStroke.modifiers != 0 && activeKeymap.getActionIds(KeyShortcut(keyStroke)).isNotEmpty()) {
return
}
@@ -163,4 +159,4 @@ class TerminalPanelKeyAdapter(
return Character.toLowerCase(e.keyCode.toChar())
}
}
}

View File

@@ -53,31 +53,30 @@ class TerminalPanelMouseSelectionAdapter(private val terminalPanel: TerminalPane
if (SwingUtilities.isRightMouseButton(e)) {
// 如果有选中并且开启了选中复制,那么右键直接是粘贴
if (selectionModel.hasSelection() && isSelectCopy.not()) {
if (rightClickMode != "Nothing") {
triggerCopyAction(
triggerCopyAction(
KeyEvent(
e.component,
KeyEvent.KEY_PRESSED,
e.`when`,
e.modifiersEx,
KeyEvent.VK_C,
'C'
)
)
if (rightClickMode == "CopyAndPaste") {
triggerPasteAction(
KeyEvent(
e.component,
KeyEvent.KEY_PRESSED,
e.`when`,
e.modifiersEx,
KeyEvent.VK_C,
'C'
KeyEvent.VK_V,
'V'
)
)
if (rightClickMode == "CopyAndPaste") {
triggerPasteAction(
KeyEvent(
e.component,
KeyEvent.KEY_PRESSED,
e.`when`,
e.modifiersEx,
KeyEvent.VK_V,
'V'
)
)
}
}
} else {
// paste
triggerPasteAction(

View File

@@ -30,8 +30,6 @@ import java.nio.file.Path
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
import javax.swing.*
import javax.swing.event.PopupMenuEvent
import javax.swing.event.PopupMenuListener
import kotlin.io.path.absolutePathString
import kotlin.math.max
import kotlin.reflect.cast
@@ -60,24 +58,12 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
private val connectFailedPanel = ConnectFailedPanel()
private val transferManager = TransferTableModel(coroutineScope)
private val disposable = Disposer.newDisposable()
private val focusedWindow get() = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusedWindow
private val owner get() = SwingUtilities.getWindowAncestor(this)
private val questionBtn = JButton(Icons.questionMark)
private val downloadBtn = JButton(Icons.download)
private val badgePresentation = Badge.getInstance(tab.windowScope)
.addBadge(downloadBtn).apply { visible = false }
private val support = DataProviderSupport()
private var isShowPopupMenu = false
override var isStickHover: Boolean
get() = super.isStickHover
set(value) {
if (isShowPopupMenu || owner != focusedWindow) {
super.isStickHover = true
} else {
super.isStickHover = value
}
}
init {
initViews()
@@ -149,8 +135,6 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
}
})
questionBtn.toolTipText = I18n.getString("termora.visual-window.transport.question")
// 立即连接
connect()
}
@@ -167,7 +151,7 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
val support = DefaultTransportSupport(fileSystem, fileSystem.defaultDir)
withContext(Dispatchers.Swing) {
val internalTransferManager = MyInternalTransferManager()
val transportPanel = object : TransportPanel(
val transportPanel = TransportPanel(
internalTransferManager, tab.host,
object : TransportSupportLoader {
override suspend fun getTransportSupport(): TransportSupport {
@@ -181,27 +165,7 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
override fun isLoaded(): Boolean {
return true
}
}) {
override fun customizeContextmenu(
rows: Array<Int>,
e: MouseEvent,
popupMenu: TransportPopupMenu
) {
popupMenu.addPopupMenuListener(object : PopupMenuListener {
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent?) {
isShowPopupMenu = true
}
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent?) {
isShowPopupMenu = false
}
override fun popupMenuCanceled(e: PopupMenuEvent?) {
isShowPopupMenu = false
}
})
}
}
})
internalTransferManager.setTransferPanel(transportPanel)
Disposer.register(transportPanel, object : Disposable {
override fun dispose() {
@@ -276,10 +240,6 @@ internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: Vi
super.dispose()
}
override fun reassemble() {
super.reassemble()
}
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return support.getData(dataKey)
}

View File

@@ -36,7 +36,6 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
private var dialog: VisualWindowDialog? = null
private var oldBounds = Rectangle()
private var toggleWindowBtn = JButton(Icons.openInNewWindow)
private val closeBtn = JButton(Icons.close)
private var isAlwaysTop
get() = properties.getString("VisualWindow.${id}.dialog.isAlwaysTop", "false").toBoolean()
set(value) = properties.putString("VisualWindow.${id}.dialog.isAlwaysTop", value.toString())
@@ -48,8 +47,8 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
}
}
protected open var isStickHover = false
set(value) {
protected var isStickHover = false
private set(value) {
if (value == field) return
field = value
reassemble()
@@ -93,8 +92,6 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
oldBounds = bounds
alwaysTopBtn.isSelected = isAlwaysTop
alwaysTopBtn.isVisible = false
closeBtn.toolTipText = I18n.getString("termora.tabbed.contextmenu.close")
}
protected open fun toolbarButtons(): List<Pair<JButton, Position>> {
@@ -137,7 +134,6 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
addMouseListener(object : MouseAdapter() {})
toggleWindowBtn.addActionListener { toggleWindow() }
toggleWindowBtn.toolTipText = I18n.getString("termora.visual-window.toggle-window")
addPropertyChangeListener("isWindow") {
if (isWindow) {
@@ -169,8 +165,6 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
dialog?.isAlwaysOnTop = isAlwaysTop
}
}
closeBtn.addActionListener { if (beforeClose()) Disposer.dispose(visualWindow) }
}
private fun initToolBar() {
@@ -186,7 +180,7 @@ open class VisualWindowPanel(protected val id: String, protected val visualWindo
buttons.filter { it.second == Position.Right }.forEach { toolbar.add(it.first) }
toolbar.add(toggleWindowBtn)
toolbar.add(closeBtn)
toolbar.add(JButton(Icons.close).apply { addActionListener { if (beforeClose()) Disposer.dispose(visualWindow) } })
toolbar.border = BorderFactory.createMatteBorder(0, 0, 1, 0, DynamicColor.BorderColor)
add(toolbar, BorderLayout.NORTH)
}

View File

@@ -66,8 +66,6 @@ class BookmarkButton : JButton(Icons.bookmarks) {
})
isBookmark = false
toolTipText = I18n.getString("termora.transport.bookmarks")
}
private fun showBookmarks(e: MouseEvent) {

View File

@@ -46,7 +46,6 @@ class TransferAnAction : AnAction(I18n.getString("termora.transport.sftp"), Icon
val panel = tabbed.getTransportPanel(i) ?: continue
if (panel.host.id == host.id) {
tabbed.selectedIndex = i
terminalTabbedManager.setSelectedTerminalTab(sftpTab)
return
}
}

View File

@@ -1,7 +1,7 @@
package app.termora.transfer
import app.termora.WindowScope
import app.termora.plugin.Extension
import java.awt.Window
import java.nio.file.FileSystem
import java.nio.file.Path
import javax.swing.JMenuItem
@@ -14,7 +14,7 @@ internal interface TransportContextMenuExtension : Extension {
* @param fileSystem 为 null 表示可能已经断线,处于不可用状态
*/
fun createJMenuItem(
window: Window,
windowScope: WindowScope,
fileSystem: FileSystem?,
popupMenu: TransportPopupMenu,
files: List<Pair<Path, TransportTableModel.Attributes>>

View File

@@ -63,7 +63,7 @@ import kotlin.io.path.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
internal open class TransportPanel(
internal class TransportPanel(
private val internalTransferManager: InternalTransferManager,
val host: Host,
val loader: TransportSupportLoader,
@@ -131,10 +131,10 @@ internal open class TransportPanel(
* 工作目录
*/
override var workdir: Path? = null
protected set
private set
override var loading = false
protected set(value) {
private set(value) {
val oldValue = field
field = value
if (oldValue != value) {
@@ -165,14 +165,6 @@ internal open class TransportPanel(
toolbar.add(eyeBtn)
toolbar.add(refreshBtn)
prevBtn.toolTipText = I18n.getString("termora.transport.toolbar.prev")
homeBtn.toolTipText = I18n.getString("termora.transport.toolbar.home")
nextBtn.toolTipText = I18n.getString("termora.transport.toolbar.next")
parentBtn.toolTipText = I18n.getString("termora.transport.toolbar.parent")
eyeBtn.toolTipText = I18n.getString("termora.transport.toolbar.show-hide")
refreshBtn.toolTipText = I18n.getString("termora.transport.toolbar.refresh")
sorter.maxSortKeys = 1
table.setRowSorter(sorter)
table.setAutoCreateRowSorter(false)
@@ -419,7 +411,7 @@ internal open class TransportPanel(
}
})
addPropertyChangeListener("workdir") { _ -> reload() }
addPropertyChangeListener("workdir") { evt -> reload() }
reload()
}
@@ -530,8 +522,7 @@ internal open class TransportPanel(
) == JOptionPane.YES_OPTION
) {
// 直接执行删除操作
val future =
internalTransferManager.addTransfer(validFiles, InternalTransferManager.TransferMode.Delete)
val future = internalTransferManager.addTransfer(validFiles, InternalTransferManager.TransferMode.Delete)
mountFuture(future)
}
}
@@ -925,18 +916,13 @@ internal open class TransportPanel(
}
}
protected open fun showContextmenu(rows: Array<Int>, e: MouseEvent) {
private fun showContextmenu(rows: Array<Int>, e: MouseEvent) {
val files = rows.map { model.getPath(it) to model.getAttributes(it) }
val popupMenu = TransportPopupMenu(owner, model, internalTransferManager, loader, files)
popupMenu.addActionListener(PopupMenuActionListener(files))
customizeContextmenu(rows, e, popupMenu)
popupMenu.show(table, e.x, e.y)
}
protected open fun customizeContextmenu(rows: Array<Int>, e: MouseEvent, popupMenu: TransportPopupMenu) {
}
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return support.getData(dataKey)
}

View File

@@ -4,7 +4,6 @@ import app.termora.*
import app.termora.plugin.ExtensionManager
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.util.SystemInfo
import kotlinx.coroutines.launch
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
@@ -42,14 +41,7 @@ internal class TransportPopupMenu(
private val copyPathMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.copy-path"))
private val copyMenu = JMenuItem(I18n.getString("termora.copy"))
private val pasteMenu = JMenuItem(I18n.getString("termora.paste"))
private val openInFinderMenu = JMenuItem(
I18n.getString(
"termora.transport.table.contextmenu.open-in-folder",
if (SystemInfo.isMacOS) I18n.getString("termora.finder")
else if (SystemInfo.isWindows) I18n.getString("termora.explorer")
else I18n.getString("termora.folder")
)
)
private val openInFinderMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.open-in-folder"))
private val renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename"))
private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete"))
@@ -108,7 +100,7 @@ internal class TransportPopupMenu(
for (extension in extensionManager.getExtensions(TransportContextMenuExtension::class.java)) {
try {
val menu = extension.createJMenuItem(
owner,
ApplicationScope.forWindowScope(owner),
fileSystem,
this,
files

View File

@@ -1,6 +1,7 @@
package app.termora.transfer.internal.sftp
import app.termora.I18n
import app.termora.WindowScope
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.randomUUID
@@ -8,7 +9,6 @@ import app.termora.transfer.*
import org.apache.commons.lang3.StringUtils
import org.apache.sshd.common.file.util.MockPath
import org.apache.sshd.sftp.client.fs.SftpFileSystem
import java.awt.Window
import java.nio.file.FileSystem
import java.nio.file.Path
import javax.swing.JMenu
@@ -23,7 +23,7 @@ internal class CompressTransportContextMenuExtension private constructor() : Tra
}
override fun createJMenuItem(
window: Window,
windowScope: WindowScope,
fileSystem: FileSystem?,
popupMenu: TransportPopupMenu,
files: List<Pair<Path, TransportTableModel.Attributes>>

View File

@@ -1,13 +1,13 @@
package app.termora.transfer.internal.sftp
import app.termora.I18n
import app.termora.WindowScope
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.randomUUID
import app.termora.transfer.*
import org.apache.commons.lang3.StringUtils
import org.apache.sshd.sftp.client.fs.SftpFileSystem
import java.awt.Window
import java.nio.file.FileSystem
import java.nio.file.Path
import javax.swing.JMenu
@@ -21,7 +21,7 @@ internal class ExtractTransportContextMenuExtension private constructor() : Tran
}
override fun createJMenuItem(
window: Window,
windowScope: WindowScope,
fileSystem: FileSystem?,
popupMenu: TransportPopupMenu,
files: List<Pair<Path, TransportTableModel.Attributes>>

View File

@@ -3,11 +3,11 @@ package app.termora.transfer.internal.sftp
import app.termora.I18n
import app.termora.Icons
import app.termora.OptionPane
import app.termora.WindowScope
import app.termora.transfer.TransportContextMenuExtension
import app.termora.transfer.TransportPopupMenu
import app.termora.transfer.TransportTableModel
import org.apache.sshd.sftp.client.fs.SftpFileSystem
import java.awt.Window
import java.nio.file.FileSystem
import java.nio.file.Path
import javax.swing.JMenuItem
@@ -19,7 +19,7 @@ internal class RmrfTransportContextMenuExtension private constructor() : Transpo
}
override fun createJMenuItem(
window: Window,
windowScope: WindowScope,
fileSystem: FileSystem?,
popupMenu: TransportPopupMenu,
files: List<Pair<Path, TransportTableModel.Attributes>>
@@ -31,7 +31,7 @@ internal class RmrfTransportContextMenuExtension private constructor() : Transpo
val rmrfMenu = JMenuItem("rm -rf", Icons.warningIntroduction)
rmrfMenu.addActionListener {
if (OptionPane.showConfirmDialog(
window,
windowScope.window,
I18n.getString("termora.transport.table.contextmenu.rm-warning"),
messageType = JOptionPane.ERROR_MESSAGE
) == JOptionPane.YES_OPTION

View File

@@ -8,8 +8,6 @@ import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.apache.sshd.client.SshClient
import org.apache.sshd.client.session.ClientSession
import org.apache.sshd.core.CoreModuleProperties
import org.apache.sshd.sftp.SftpModuleProperties
import org.apache.sshd.sftp.client.SftpClientFactory
internal class SFTPTransferProtocolProvider : TransferProtocolProvider {
@@ -34,11 +32,6 @@ internal class SFTPTransferProtocolProvider : TransferProtocolProvider {
client = if (owner == null) SshClients.openClient(requester.host)
else SshClients.openClient(requester.host, owner)
session = SshClients.openSession(requester.host, client)
CoreModuleProperties.IO_CONNECT_TIMEOUT.get(client).ifPresent { e ->
SftpModuleProperties.SFTP_CHANNEL_OPEN_TIMEOUT.set(session, e)
}
val fileSystem = SftpClientFactory.instance().createSftpFileSystem(session)
val host = requester.host

View File

@@ -146,26 +146,6 @@ 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)
@@ -213,9 +193,6 @@ 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))
@@ -1174,4 +1151,4 @@ class NewHostTree : SimpleTree(), Disposable {
}
}
}

View File

@@ -59,7 +59,6 @@ termora.settings.terminal.hyperlink=Hyperlink
termora.settings.terminal.select-copy=Select copy
termora.settings.terminal.right-click=Right click
termora.settings.terminal.right-click.copy-and-paste=Copy and Paste
termora.settings.terminal.right-click.nothing=Nothing
termora.settings.terminal.right-click.copy=${termora.copy}
termora.settings.terminal.cursor-style=Cursor type
termora.settings.terminal.cursor-blink=Cursor blink
@@ -90,9 +89,11 @@ 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.wait-login=Waiting for login in the default browser...
termora.settings.account.mfa=MFA is optional
termora.settings.account.locally=locally
termora.settings.account.lifetime=Lifetime
termora.settings.account.upgrade=Upgrade
@@ -115,7 +116,7 @@ termora.settings.keymap=Keymap
termora.settings.keymap.shortcut=Shortcut
termora.settings.keymap.action=Action
termora.settings.keymap.already-exists=The shortcut [{0}] is already in use by [{1}]
termora.settings.keymap.question=Select a line, press the <font color=rgb({0},{1},{2})> ⌫ (Backspace)</font> key to remove the shortcut
termora.settings.sftp.edit-command=Edit Command
termora.settings.sftp.db-click-behavior=Double-click
@@ -310,14 +311,6 @@ termora.tools.multiple=Send command to the current window sessions
termora.transport.local=Local
termora.transport.file-already-exists=The file {0} already exists
termora.transport.toolbar.prev=Backward
termora.transport.toolbar.home=Home Folder
termora.transport.toolbar.next=Forward
termora.transport.toolbar.parent=Parent Folder
termora.transport.toolbar.show-hide=Show/Hide Folders
termora.transport.toolbar.refresh=Refresh Folder
termora.transport.bookmarks=Bookmarks Manager
termora.transport.bookmarks.up=Up
termora.transport.bookmarks.down=Down
@@ -334,7 +327,7 @@ termora.transport.table.owner=Owner
termora.transport.table.contextmenu.transfer=Transfer
termora.transport.table.contextmenu.edit=${termora.keymgr.edit}
termora.transport.table.contextmenu.copy-path=Copy Path
termora.transport.table.contextmenu.open-in-folder=Open in {0}
termora.transport.table.contextmenu.open-in-folder=Open in ${termora.finder}
termora.transport.table.contextmenu.rename=${termora.welcome.contextmenu.rename}
termora.transport.table.contextmenu.delete=${termora.remove}
termora.transport.table.contextmenu.rm-warning=Using the rm -rf command to delete a file is very dangerous
@@ -440,8 +433,6 @@ termora.visual-window.system-information.mem=Mem
termora.visual-window.system-information.swap=Swap
termora.visual-window.system-information.filesystem=Filesystem
termora.visual-window.system-information.used-total=Used / Total
termora.visual-window.toggle-window=Toggle window
termora.visual-window.transport.question=More Features
termora.visual-window.nvidia-smi=NVIDIA SMI

View File

@@ -49,26 +49,7 @@ 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=Терминал
@@ -81,7 +62,6 @@ termora.settings.terminal.hyperlink=Ссылки
termora.settings.terminal.select-copy=Копировать выделенное
termora.settings.terminal.right-click=правой кнопкой мыши
termora.settings.terminal.right-click.copy-and-paste=Копировать и вставить
termora.settings.terminal.right-click.nothing=никто
termora.settings.terminal.cursor-style=Вид курсора
termora.settings.terminal.cursor-blink=Мигать курсором
termora.settings.terminal.local-shell=Локальный терминал
@@ -100,7 +80,7 @@ termora.settings.keymap=Горяиче клавиши
termora.settings.keymap.shortcut=Комбинация
termora.settings.keymap.action=Команда
termora.settings.keymap.already-exists=Комбинация [{0}] уже используется [{1}]
termora.settings.keymap.question=Выберите строку и нажмите <font color=rgb({0},{1},{2})> ⌫ (клавишу Backspace)</font>, чтобы удалить сочетание клавиш
termora.settings.sftp.edit-command=Редактировать команду
termora.settings.sftp.db-click-behavior=двойной щелчок
@@ -271,14 +251,6 @@ termora.transport.bookmarks=Менеджер закладок
termora.transport.bookmarks.up=Вверх
termora.transport.bookmarks.down=Вниз
termora.transport.toolbar.prev=Назад
termora.transport.toolbar.home=Домашняя папка
termora.transport.toolbar.next=Вперёд
termora.transport.toolbar.parent=Родительская папка
termora.transport.toolbar.show-hide=Показать/Скрыть папки
termora.transport.toolbar.refresh=Обновить
termora.transport.table.filename=Имя файла
termora.transport.table.type=Тип
termora.transport.table.type.symbolic-link=Символьная Ссылка
@@ -386,8 +358,6 @@ termora.visual-window.system-information.mem=Память
termora.visual-window.system-information.swap=Подкачка
termora.visual-window.system-information.filesystem=Файловая система
termora.visual-window.system-information.used-total=Использовано / Всего
termora.visual-window.toggle-window=Переключить окно
termora.visual-window.transport.question=Больше возможностей
termora.visual-window.nvidia-smi=NVIDIA SMI

View File

@@ -73,7 +73,6 @@ termora.settings.terminal.hyperlink=超链接
termora.settings.terminal.select-copy=选中复制
termora.settings.terminal.right-click=右键点击
termora.settings.terminal.right-click.copy-and-paste=复制 & 粘贴
termora.settings.terminal.right-click.nothing=无操作
termora.settings.terminal.cursor-style=光标样式
termora.settings.terminal.cursor-blink=光标闪烁
termora.settings.terminal.local-shell=本地终端
@@ -104,8 +103,10 @@ 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.wait-login=正在等待默认浏览器中登录...
termora.settings.account.mfa=多因素验证是可选的
termora.settings.account.locally=本地的
termora.settings.account.lifetime=长期
termora.settings.account.verify=验证
@@ -126,7 +127,7 @@ termora.settings.keymap=键盘
termora.settings.keymap.shortcut=快捷键
termora.settings.keymap.action=操作
termora.settings.keymap.already-exists=快捷键 [{0}] 已经被 [{1}] 占用
termora.settings.keymap.question=选中一行,按下 <font color=rgb({0},{1},{2})> ⌫ (退格键)</font> 移除快捷键
termora.settings.sftp.edit-command=编辑命令
termora.settings.sftp.db-click-behavior=双击行为
@@ -311,15 +312,6 @@ termora.transport.bookmarks=书签管理
termora.transport.bookmarks.up=上移
termora.transport.bookmarks.down=下移
termora.transport.toolbar.prev=返回
termora.transport.toolbar.home=默认目录
termora.transport.toolbar.next=前进
termora.transport.toolbar.parent=父目录
termora.transport.toolbar.show-hide=显示/隐藏目录
termora.transport.toolbar.refresh=刷新
termora.transport.table.filename=文件名
termora.transport.table.type=类型
termora.transport.table.size=大小
@@ -331,7 +323,7 @@ termora.transport.table.owner=所有者
# contextmenu
termora.transport.table.contextmenu.transfer=传输
termora.transport.table.contextmenu.copy-path=复制路径
termora.transport.table.contextmenu.open-in-folder={0}中打开
termora.transport.table.contextmenu.open-in-folder=${termora.finder}中打开
termora.transport.table.contextmenu.change-permissions=更改权限...
termora.transport.table.contextmenu.refresh=刷新
termora.transport.table.contextmenu.compress=压缩
@@ -437,8 +429,6 @@ termora.visual-window.system-information.mem=内存
termora.visual-window.system-information.swap=交换
termora.visual-window.system-information.filesystem=文件系统
termora.visual-window.system-information.used-total=使用 / 大小
termora.visual-window.toggle-window=切换窗口
termora.visual-window.transport.question=更多功能
termora.floating-toolbar.close-in-current-tab=在当前标签页关闭

View File

@@ -55,7 +55,6 @@ termora.settings.keymap=鍵盤
termora.settings.keymap.shortcut=快捷鍵
termora.settings.keymap.action=操作
termora.settings.keymap.already-exists=快捷鍵 [{0}] 已經被 [{1}] 占用
termora.settings.keymap.question=選取一行,按下 <font color=rgb({0},{1},{2})> ⌫ (退格鍵)</font> 移除快速鍵
termora.settings.sftp.edit-command=編輯命令
termora.settings.sftp.db-click-behavior=按兩下行為
@@ -85,7 +84,6 @@ termora.settings.terminal.hyperlink=超連結
termora.settings.terminal.select-copy=選取複製
termora.settings.terminal.right-click=右鍵點擊
termora.settings.terminal.right-click.copy-and-paste=複製 & 貼上
termora.settings.terminal.right-click.nothing=無操作
termora.settings.terminal.cursor-style=遊標風格
termora.settings.terminal.cursor-blink=遊標閃爍
termora.settings.terminal.local-shell=本地端
@@ -116,8 +114,10 @@ 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.wait-login=正在等待預設瀏覽器登入...
termora.settings.account.mfa=多因素驗證是可選的
termora.settings.account.locally=本地的
termora.settings.account.lifetime=長期
termora.settings.account.verify=驗證
@@ -307,13 +307,6 @@ termora.transport.bookmarks=書籤管理
termora.transport.bookmarks.up=上移
termora.transport.bookmarks.down=下移
termora.transport.toolbar.prev=返回
termora.transport.toolbar.home=預設目錄
termora.transport.toolbar.next=前進
termora.transport.toolbar.parent=父目錄
termora.transport.toolbar.show-hide=顯示/隱藏目錄
termora.transport.toolbar.refresh=重新整理
termora.transport.table.filename=檔名
termora.transport.table.type=類型
termora.transport.table.size=大小
@@ -325,7 +318,7 @@ termora.transport.table.owner=所有者
# contextmenu
termora.transport.table.contextmenu.transfer=傳輸
termora.transport.table.contextmenu.copy-path=複製路徑
termora.transport.table.contextmenu.open-in-folder={0}中打開
termora.transport.table.contextmenu.open-in-folder=${termora.finder}中打開
termora.transport.table.contextmenu.change-permissions=更改權限...
termora.transport.table.contextmenu.refresh=刷新
termora.transport.table.contextmenu.compress=壓縮
@@ -423,8 +416,6 @@ termora.visual-window.system-information.mem=內存
termora.visual-window.system-information.swap=交換
termora.visual-window.system-information.filesystem=檔案系統
termora.visual-window.system-information.used-total=使用 / 大小
termora.visual-window.toggle-window=切換視窗
termora.visual-window.transport.question=更多功能
termora.floating-toolbar.close-in-current-tab=在目前標籤頁關閉

View File

@@ -15,19 +15,6 @@ 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)

View File

@@ -1,5 +1,5 @@
FROM linuxserver/openssh-server:9.3_p2-r1-ls147
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
FROM linuxserver/openssh-server
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 \
&& ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz
@@ -7,6 +7,3 @@ RUN sed -i 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/ssh
RUN sed -i 's/AllowTcpForwarding no/AllowTcpForwarding yes/g' /etc/ssh/sshd_config
RUN sed -i 's/GatewayPorts no/GatewayPorts yes/g' /etc/ssh/sshd_config
RUN sed -i 's/X11Forwarding no/X11Forwarding yes/g' /etc/ssh/sshd_config
# docker build -t sshd .
# docker run --rm -it -e SUDO_ACCESS=true -e PASSWORD_ACCESS=true -e USER_PASSWORD=123456 -p 2222:2222 sshd