From e1955a371ec70d1a598423e0e09109f3e1a0257c Mon Sep 17 00:00:00 2001 From: hstyi Date: Thu, 6 Feb 2025 16:03:25 +0800 Subject: [PATCH] feat: support for WebDAV (#150) --- .../kotlin/app/termora/SettingsOptionsPane.kt | 70 +++- src/main/kotlin/app/termora/sync/GitSyncer.kt | 288 +---------------- .../kotlin/app/termora/sync/SafetySyncer.kt | 299 ++++++++++++++++++ .../kotlin/app/termora/sync/SyncConfig.kt | 1 + .../kotlin/app/termora/sync/SyncerProvider.kt | 1 + .../kotlin/app/termora/sync/WebDAVSyncer.kt | 152 +++++++++ src/main/resources/i18n/messages.properties | 1 + .../resources/i18n/messages_zh_CN.properties | 1 + .../resources/i18n/messages_zh_TW.properties | 1 + 9 files changed, 522 insertions(+), 292 deletions(-) create mode 100644 src/main/kotlin/app/termora/sync/SafetySyncer.kt create mode 100644 src/main/kotlin/app/termora/sync/WebDAVSyncer.kt diff --git a/src/main/kotlin/app/termora/SettingsOptionsPane.kt b/src/main/kotlin/app/termora/SettingsOptionsPane.kt index b7a1128..e821fe8 100644 --- a/src/main/kotlin/app/termora/SettingsOptionsPane.kt +++ b/src/main/kotlin/app/termora/SettingsOptionsPane.kt @@ -550,12 +550,6 @@ class SettingsOptionsPane : OptionsPane() { } } - if (typeComboBox.selectedItem == SyncType.Gitee) { - gistTextField.trailingComponent = null - } else { - gistTextField.trailingComponent = visitGistBtn - } - removeAll() add(getCenterComponent(), BorderLayout.CENTER) revalidate() @@ -987,6 +981,7 @@ class SettingsOptionsPane : OptionsPane() { typeComboBox.addItem(SyncType.GitHub) typeComboBox.addItem(SyncType.GitLab) typeComboBox.addItem(SyncType.Gitee) + typeComboBox.addItem(SyncType.WebDAV) hostsCheckBox.isFocusable = false keysCheckBox.isFocusable = false @@ -1005,7 +1000,31 @@ class SettingsOptionsPane : OptionsPane() { tokenTextField.text = sync.token domainTextField.trailingComponent = JButton(Icons.externalLink).apply { addActionListener { - Application.browse(URI.create("https://docs.gitlab.com/ee/api/snippets.html")) + if (typeComboBox.selectedItem == SyncType.GitLab) { + Application.browse(URI.create("https://docs.gitlab.com/ee/api/snippets.html")) + + } else if (typeComboBox.selectedItem == SyncType.WebDAV) { + val url = domainTextField.text + if (url.isNullOrBlank()) { + OptionPane.showMessageDialog( + owner, + I18n.getString("termora.settings.sync.webdav.help") + ) + } else { + val uri = URI.create(url) + val sb = StringBuilder() + sb.append(uri.scheme).append("://") + if (tokenTextField.password.isNotEmpty() && gistTextField.text.isNotBlank()) { + sb.append(String(tokenTextField.password)).append(":").append(gistTextField.text) + sb.append('@') + } + sb.append(uri.authority).append(uri.path) + if (!uri.query.isNullOrBlank()) { + sb.append('?').append(uri.query) + } + Application.browse(URI.create(sb.toString())) + } + } } } @@ -1015,12 +1034,15 @@ class SettingsOptionsPane : OptionsPane() { tokenTextField.trailingComponent = if (tokenTextField.password.isEmpty()) getTokenBtn else null - if (typeComboBox.selectedItem == SyncType.GitLab) { - if (domainTextField.text.isBlank()) { + if (domainTextField.text.isBlank()) { + if (typeComboBox.selectedItem == SyncType.GitLab) { domainTextField.text = StringUtils.defaultIfBlank(sync.domain, "https://gitlab.com/api") + } else if (typeComboBox.selectedItem == SyncType.WebDAV) { + domainTextField.text = sync.domain } } + val lastSyncTime = sync.lastSyncTime lastSyncTimeLabel.text = "${I18n.getString("termora.settings.sync.last-sync-time")}: ${ if (lastSyncTime > 0) DateFormatUtils.format( @@ -1069,17 +1091,37 @@ class SettingsOptionsPane : OptionsPane() { val builder = FormBuilder.create().layout(layout).debug(false) val box = Box.createHorizontalBox() box.add(typeComboBox) - if (typeComboBox.selectedItem == SyncType.GitLab) { + if (typeComboBox.selectedItem == SyncType.GitLab || typeComboBox.selectedItem == SyncType.WebDAV) { box.add(Box.createHorizontalStrut(4)) box.add(domainTextField) } builder.add("${I18n.getString("termora.settings.sync.type")}:").xy(1, rows) .add(box).xy(3, rows).apply { rows += step } - builder.add("${I18n.getString("termora.settings.sync.token")}:").xy(1, rows) - .add(tokenTextField).xy(3, rows).apply { rows += step } - .add("${I18n.getString("termora.settings.sync.gist")}:").xy(1, rows) - .add(gistTextField).xy(3, rows).apply { rows += step } + val isWebDAV = typeComboBox.selectedItem == SyncType.WebDAV + + val tokenText = if (isWebDAV) { + I18n.getString("termora.new-host.general.username") + } else { + I18n.getString("termora.settings.sync.token") + } + + val gistText = if (isWebDAV) { + I18n.getString("termora.new-host.general.password") + } else { + I18n.getString("termora.settings.sync.gist") + } + + if (typeComboBox.selectedItem == SyncType.Gitee || isWebDAV) { + gistTextField.trailingComponent = null + } else { + gistTextField.trailingComponent = visitGistBtn + } + + builder.add("${tokenText}:").xy(1, rows) + .add(if (isWebDAV) gistTextField else tokenTextField).xy(3, rows).apply { rows += step } + .add("${gistText}:").xy(1, rows) + .add(if (isWebDAV) tokenTextField else gistTextField).xy(3, rows).apply { rows += step } .add("${I18n.getString("termora.settings.sync.range")}:").xy(1, rows) .add(rangeBox).xy(3, rows).apply { rows += step } // Sync buttons diff --git a/src/main/kotlin/app/termora/sync/GitSyncer.kt b/src/main/kotlin/app/termora/sync/GitSyncer.kt index 53c249d..7bab2d2 100644 --- a/src/main/kotlin/app/termora/sync/GitSyncer.kt +++ b/src/main/kotlin/app/termora/sync/GitSyncer.kt @@ -1,42 +1,19 @@ package app.termora.sync -import app.termora.* -import app.termora.AES.CBC.aesCBCDecrypt -import app.termora.AES.CBC.aesCBCEncrypt -import app.termora.AES.decodeBase64 -import app.termora.AES.encodeBase64String import app.termora.Application.ohMyJson -import app.termora.highlight.KeywordHighlight -import app.termora.highlight.KeywordHighlightManager -import app.termora.keymap.Keymap -import app.termora.keymap.KeymapManager -import app.termora.keymgr.KeyManager -import app.termora.keymgr.OhKeyPair -import app.termora.macro.Macro -import app.termora.macro.MacroManager -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.JsonObject +import app.termora.ResponseException import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import okhttp3.Request import okhttp3.Response import org.apache.commons.lang3.ArrayUtils import org.slf4j.LoggerFactory -import javax.swing.SwingUtilities -abstract class GitSyncer : Syncer { +abstract class GitSyncer : SafetySyncer() { companion object { private val log = LoggerFactory.getLogger(GitSyncer::class.java) } - protected val description = "${Application.getName()} config" - protected val httpClient get() = Application.httpClient - protected val hostManager get() = HostManager.getInstance() - protected val keyManager get() = KeyManager.getInstance() - protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance() - protected val macroManager get() = MacroManager.getInstance() - protected val keymapManager get() = KeymapManager.getInstance() - override fun pull(config: SyncConfig): GistResponse { if (log.isInfoEnabled) { @@ -92,174 +69,6 @@ abstract class GitSyncer : Syncer { return gistResponse } - private fun decodeHosts(text: String, config: SyncConfig) { - // aes key - val key = getKey(config) - val encryptedHosts = ohMyJson.decodeFromString>(text) - val hosts = hostManager.hosts().associateBy { it.id } - - for (encryptedHost in encryptedHosts) { - val oldHost = hosts[encryptedHost.id] - - // 如果一样,则无需配置 - if (oldHost != null) { - if (oldHost.updateDate == encryptedHost.updateDate) { - continue - } - } - - try { - // aes iv - val iv = getIv(encryptedHost.id) - val host = Host( - id = encryptedHost.id, - name = encryptedHost.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), - protocol = Protocol.valueOf( - encryptedHost.protocol.decodeBase64().aesCBCDecrypt(key, iv).decodeToString() - ), - host = encryptedHost.host.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), - port = encryptedHost.port.decodeBase64().aesCBCDecrypt(key, iv) - .decodeToString().toIntOrNull() ?: 0, - username = encryptedHost.username.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), - remark = encryptedHost.remark.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), - authentication = ohMyJson.decodeFromString( - encryptedHost.authentication.decodeBase64().aesCBCDecrypt(key, iv).decodeToString() - ), - proxy = ohMyJson.decodeFromString( - encryptedHost.proxy.decodeBase64().aesCBCDecrypt(key, iv).decodeToString() - ), - options = ohMyJson.decodeFromString( - encryptedHost.options.decodeBase64().aesCBCDecrypt(key, iv).decodeToString() - ), - tunnelings = ohMyJson.decodeFromString( - encryptedHost.tunnelings.decodeBase64().aesCBCDecrypt(key, iv).decodeToString() - ), - sort = encryptedHost.sort, - parentId = encryptedHost.parentId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), - ownerId = encryptedHost.ownerId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), - creatorId = encryptedHost.creatorId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), - createDate = encryptedHost.createDate, - updateDate = encryptedHost.updateDate, - deleted = encryptedHost.deleted - ) - SwingUtilities.invokeLater { hostManager.addHost(host) } - } catch (e: Exception) { - if (log.isWarnEnabled) { - log.warn("Decode host: ${encryptedHost.id} failed. error: {}", e.message, e) - } - } - } - - - if (log.isDebugEnabled) { - log.debug("Decode hosts: {}", text) - } - } - - private fun decodeKeys(text: String, config: SyncConfig) { - // aes key - val key = getKey(config) - val encryptedKeys = ohMyJson.decodeFromString>(text) - - for (encryptedKey in encryptedKeys) { - try { - // aes iv - val iv = getIv(encryptedKey.id) - val keyPair = OhKeyPair( - id = encryptedKey.id, - publicKey = encryptedKey.publicKey.decodeBase64().aesCBCDecrypt(key, iv).encodeBase64String(), - privateKey = encryptedKey.privateKey.decodeBase64().aesCBCDecrypt(key, iv).encodeBase64String(), - type = encryptedKey.type.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), - name = encryptedKey.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), - remark = encryptedKey.remark.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), - length = encryptedKey.length, - sort = encryptedKey.sort - ) - SwingUtilities.invokeLater { keyManager.addOhKeyPair(keyPair) } - } catch (e: Exception) { - if (log.isWarnEnabled) { - log.warn("Decode key: ${encryptedKey.id} failed. error: {}", e.message, e) - } - } - } - - if (log.isDebugEnabled) { - log.debug("Decode keys: {}", text) - } - } - - private fun decodeKeywordHighlights(text: String, config: SyncConfig) { - // aes key - val key = getKey(config) - val encryptedKeywordHighlights = ohMyJson.decodeFromString>(text) - - for (e in encryptedKeywordHighlights) { - try { - // aes iv - val iv = getIv(e.id) - keywordHighlightManager.addKeywordHighlight( - e.copy( - keyword = e.keyword.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), - description = e.description.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), - ) - ) - } catch (ex: Exception) { - if (log.isWarnEnabled) { - log.warn("Decode KeywordHighlight: ${e.id} failed. error: {}", ex.message, ex) - } - } - } - - if (log.isDebugEnabled) { - log.debug("Decode KeywordHighlight: {}", text) - } - } - - private fun decodeMacros(text: String, config: SyncConfig) { - // aes key - val key = getKey(config) - val encryptedMacros = ohMyJson.decodeFromString>(text) - - for (e in encryptedMacros) { - try { - // aes iv - val iv = getIv(e.id) - macroManager.addMacro( - e.copy( - name = e.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), - macro = e.macro.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), - ) - ) - } catch (ex: Exception) { - if (log.isWarnEnabled) { - log.warn("Decode Macro: ${e.id} failed. error: {}", ex.message, ex) - } - } - } - - if (log.isDebugEnabled) { - log.debug("Decode Macros: {}", text) - } - } - - private fun decodeKeymaps(text: String, config: SyncConfig) { - - for (keymap in ohMyJson.decodeFromString>(text).mapNotNull { Keymap.fromJSON(it) }) { - keymapManager.addKeymap(keymap) - } - - if (log.isDebugEnabled) { - log.debug("Decode Keymaps: {}", text) - } - } - - private fun getKey(config: SyncConfig): ByteArray { - return ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16) - } - - private fun getIv(id: String): ByteArray { - return ArrayUtils.subarray(id.padEnd(16, '0').toByteArray(), 0, 16) - } override fun push(config: SyncConfig): GistResponse { val gistFiles = mutableListOf() @@ -268,62 +77,16 @@ abstract class GitSyncer : Syncer { // Hosts if (config.ranges.contains(SyncRange.Hosts)) { - val encryptedHosts = mutableListOf() - for (host in hostManager.hosts()) { - // aes iv - val iv = ArrayUtils.subarray(host.id.padEnd(16, '0').toByteArray(), 0, 16) - val encryptedHost = EncryptedHost() - encryptedHost.id = host.id - encryptedHost.name = host.name.aesCBCEncrypt(key, iv).encodeBase64String() - encryptedHost.protocol = host.protocol.name.aesCBCEncrypt(key, iv).encodeBase64String() - encryptedHost.host = host.host.aesCBCEncrypt(key, iv).encodeBase64String() - encryptedHost.port = "${host.port}".aesCBCEncrypt(key, iv).encodeBase64String() - encryptedHost.username = host.username.aesCBCEncrypt(key, iv).encodeBase64String() - encryptedHost.remark = host.remark.aesCBCEncrypt(key, iv).encodeBase64String() - encryptedHost.authentication = ohMyJson.encodeToString(host.authentication) - .aesCBCEncrypt(key, iv).encodeBase64String() - encryptedHost.proxy = ohMyJson.encodeToString(host.proxy).aesCBCEncrypt(key, iv).encodeBase64String() - encryptedHost.options = - ohMyJson.encodeToString(host.options).aesCBCEncrypt(key, iv).encodeBase64String() - encryptedHost.tunnelings = - ohMyJson.encodeToString(host.tunnelings).aesCBCEncrypt(key, iv).encodeBase64String() - encryptedHost.sort = host.sort - encryptedHost.deleted = host.deleted - encryptedHost.parentId = host.parentId.aesCBCEncrypt(key, iv).encodeBase64String() - encryptedHost.ownerId = host.ownerId.aesCBCEncrypt(key, iv).encodeBase64String() - encryptedHost.creatorId = host.creatorId.aesCBCEncrypt(key, iv).encodeBase64String() - encryptedHost.createDate = host.createDate - encryptedHost.updateDate = host.updateDate - encryptedHosts.add(encryptedHost) - } - - val hostsContent = ohMyJson.encodeToString(encryptedHosts) + val hostsContent = encodeHosts(key) if (log.isDebugEnabled) { log.debug("Push encryptedHosts: {}", hostsContent) } gistFiles.add(GistFile("Hosts", hostsContent)) - } // KeyPairs if (config.ranges.contains(SyncRange.KeyPairs)) { - val encryptedKeys = mutableListOf() - for (keyPair in keyManager.getOhKeyPairs()) { - // aes iv - val iv = ArrayUtils.subarray(keyPair.id.padEnd(16, '0').toByteArray(), 0, 16) - val encryptedKeyPair = OhKeyPair( - id = keyPair.id, - publicKey = keyPair.publicKey.decodeBase64().aesCBCEncrypt(key, iv).encodeBase64String(), - privateKey = keyPair.privateKey.decodeBase64().aesCBCEncrypt(key, iv).encodeBase64String(), - type = keyPair.type.aesCBCEncrypt(key, iv).encodeBase64String(), - name = keyPair.name.aesCBCEncrypt(key, iv).encodeBase64String(), - remark = keyPair.remark.aesCBCEncrypt(key, iv).encodeBase64String(), - length = keyPair.length, - sort = keyPair.sort - ) - encryptedKeys.add(encryptedKeyPair) - } - val keysContent = ohMyJson.encodeToString(encryptedKeys) + val keysContent = encodeKeys(key) if (log.isDebugEnabled) { log.debug("Push encryptedKeys: {}", keysContent) } @@ -332,17 +95,7 @@ abstract class GitSyncer : Syncer { // Highlights if (config.ranges.contains(SyncRange.KeywordHighlights)) { - val keywordHighlights = mutableListOf() - for (keywordHighlight in keywordHighlightManager.getKeywordHighlights()) { - // aes iv - val iv = getIv(keywordHighlight.id) - val encryptedKeyPair = keywordHighlight.copy( - keyword = keywordHighlight.keyword.aesCBCEncrypt(key, iv).encodeBase64String(), - description = keywordHighlight.description.aesCBCEncrypt(key, iv).encodeBase64String(), - ) - keywordHighlights.add(encryptedKeyPair) - } - val keywordHighlightsContent = ohMyJson.encodeToString(keywordHighlights) + val keywordHighlightsContent = encodeKeywordHighlights(key) if (log.isDebugEnabled) { log.debug("Push keywordHighlights: {}", keywordHighlightsContent) } @@ -351,17 +104,7 @@ abstract class GitSyncer : Syncer { // Macros if (config.ranges.contains(SyncRange.Macros)) { - val macros = mutableListOf() - for (macro in macroManager.getMacros()) { - val iv = getIv(macro.id) - macros.add( - macro.copy( - name = macro.name.aesCBCEncrypt(key, iv).encodeBase64String(), - macro = macro.macro.aesCBCEncrypt(key, iv).encodeBase64String() - ) - ) - } - val macrosContent = ohMyJson.encodeToString(macros) + val macrosContent = encodeMacros(key) if (log.isDebugEnabled) { log.debug("Push macros: {}", macrosContent) } @@ -370,22 +113,11 @@ abstract class GitSyncer : Syncer { // Keymap if (config.ranges.contains(SyncRange.Keymap)) { - val keymaps = mutableListOf() - for (keymap in keymapManager.getKeymaps()) { - // 只读的是内置的 - if (keymap.isReadonly) { - continue - } - keymaps.add(keymap.toJSONObject()) - } - - if (keymaps.isNotEmpty()) { - val keymapsContent = ohMyJson.encodeToString(keymaps) - if (log.isDebugEnabled) { - log.debug("Push keymaps: {}", keymapsContent) - } - gistFiles.add(GistFile("Keymaps", keymapsContent)) + val keymapsContent = encodeKeymaps() + if (log.isDebugEnabled) { + log.debug("Push keymaps: {}", keymapsContent) } + gistFiles.add(GistFile("Keymaps", keymapsContent)) } if (gistFiles.isEmpty()) { diff --git a/src/main/kotlin/app/termora/sync/SafetySyncer.kt b/src/main/kotlin/app/termora/sync/SafetySyncer.kt new file mode 100644 index 0000000..deb2fe7 --- /dev/null +++ b/src/main/kotlin/app/termora/sync/SafetySyncer.kt @@ -0,0 +1,299 @@ +package app.termora.sync + +import app.termora.* +import app.termora.AES.CBC.aesCBCDecrypt +import app.termora.AES.CBC.aesCBCEncrypt +import app.termora.AES.decodeBase64 +import app.termora.AES.encodeBase64String +import app.termora.Application.ohMyJson +import app.termora.highlight.KeywordHighlight +import app.termora.highlight.KeywordHighlightManager +import app.termora.keymap.Keymap +import app.termora.keymap.KeymapManager +import app.termora.keymgr.KeyManager +import app.termora.keymgr.OhKeyPair +import app.termora.macro.Macro +import app.termora.macro.MacroManager +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.JsonObject +import org.apache.commons.lang3.ArrayUtils +import org.slf4j.LoggerFactory +import javax.swing.SwingUtilities + +abstract class SafetySyncer : Syncer { + companion object { + private val log = LoggerFactory.getLogger(SafetySyncer::class.java) + } + + protected val description = "${Application.getName()} config" + protected val httpClient get() = Application.httpClient + protected val hostManager get() = HostManager.getInstance() + protected val keyManager get() = KeyManager.getInstance() + protected val keywordHighlightManager get() = KeywordHighlightManager.getInstance() + protected val macroManager get() = MacroManager.getInstance() + protected val keymapManager get() = KeymapManager.getInstance() + + protected fun decodeHosts(text: String, config: SyncConfig) { + // aes key + val key = getKey(config) + val encryptedHosts = ohMyJson.decodeFromString>(text) + val hosts = hostManager.hosts().associateBy { it.id } + + for (encryptedHost in encryptedHosts) { + val oldHost = hosts[encryptedHost.id] + + // 如果一样,则无需配置 + if (oldHost != null) { + if (oldHost.updateDate == encryptedHost.updateDate) { + continue + } + } + + try { + // aes iv + val iv = getIv(encryptedHost.id) + val host = Host( + id = encryptedHost.id, + name = encryptedHost.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), + protocol = Protocol.valueOf( + encryptedHost.protocol.decodeBase64().aesCBCDecrypt(key, iv).decodeToString() + ), + host = encryptedHost.host.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), + port = encryptedHost.port.decodeBase64().aesCBCDecrypt(key, iv) + .decodeToString().toIntOrNull() ?: 0, + username = encryptedHost.username.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), + remark = encryptedHost.remark.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), + authentication = ohMyJson.decodeFromString( + encryptedHost.authentication.decodeBase64().aesCBCDecrypt(key, iv).decodeToString() + ), + proxy = ohMyJson.decodeFromString( + encryptedHost.proxy.decodeBase64().aesCBCDecrypt(key, iv).decodeToString() + ), + options = ohMyJson.decodeFromString( + encryptedHost.options.decodeBase64().aesCBCDecrypt(key, iv).decodeToString() + ), + tunnelings = ohMyJson.decodeFromString( + encryptedHost.tunnelings.decodeBase64().aesCBCDecrypt(key, iv).decodeToString() + ), + sort = encryptedHost.sort, + parentId = encryptedHost.parentId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), + ownerId = encryptedHost.ownerId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), + creatorId = encryptedHost.creatorId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), + createDate = encryptedHost.createDate, + updateDate = encryptedHost.updateDate, + deleted = encryptedHost.deleted + ) + SwingUtilities.invokeLater { hostManager.addHost(host) } + } catch (e: Exception) { + if (log.isWarnEnabled) { + log.warn("Decode host: ${encryptedHost.id} failed. error: {}", e.message, e) + } + } + } + + + if (log.isDebugEnabled) { + log.debug("Decode hosts: {}", text) + } + } + + protected fun encodeHosts(key: ByteArray): String { + val encryptedHosts = mutableListOf() + for (host in hostManager.hosts()) { + // aes iv + val iv = ArrayUtils.subarray(host.id.padEnd(16, '0').toByteArray(), 0, 16) + val encryptedHost = EncryptedHost() + encryptedHost.id = host.id + encryptedHost.name = host.name.aesCBCEncrypt(key, iv).encodeBase64String() + encryptedHost.protocol = host.protocol.name.aesCBCEncrypt(key, iv).encodeBase64String() + encryptedHost.host = host.host.aesCBCEncrypt(key, iv).encodeBase64String() + encryptedHost.port = "${host.port}".aesCBCEncrypt(key, iv).encodeBase64String() + encryptedHost.username = host.username.aesCBCEncrypt(key, iv).encodeBase64String() + encryptedHost.remark = host.remark.aesCBCEncrypt(key, iv).encodeBase64String() + encryptedHost.authentication = ohMyJson.encodeToString(host.authentication) + .aesCBCEncrypt(key, iv).encodeBase64String() + encryptedHost.proxy = ohMyJson.encodeToString(host.proxy).aesCBCEncrypt(key, iv).encodeBase64String() + encryptedHost.options = + ohMyJson.encodeToString(host.options).aesCBCEncrypt(key, iv).encodeBase64String() + encryptedHost.tunnelings = + ohMyJson.encodeToString(host.tunnelings).aesCBCEncrypt(key, iv).encodeBase64String() + encryptedHost.sort = host.sort + encryptedHost.deleted = host.deleted + encryptedHost.parentId = host.parentId.aesCBCEncrypt(key, iv).encodeBase64String() + encryptedHost.ownerId = host.ownerId.aesCBCEncrypt(key, iv).encodeBase64String() + encryptedHost.creatorId = host.creatorId.aesCBCEncrypt(key, iv).encodeBase64String() + encryptedHost.createDate = host.createDate + encryptedHost.updateDate = host.updateDate + encryptedHosts.add(encryptedHost) + } + + return ohMyJson.encodeToString(encryptedHosts) + + } + + protected fun decodeKeys(text: String, config: SyncConfig) { + // aes key + val key = getKey(config) + val encryptedKeys = ohMyJson.decodeFromString>(text) + + for (encryptedKey in encryptedKeys) { + try { + // aes iv + val iv = getIv(encryptedKey.id) + val keyPair = OhKeyPair( + id = encryptedKey.id, + publicKey = encryptedKey.publicKey.decodeBase64().aesCBCDecrypt(key, iv).encodeBase64String(), + privateKey = encryptedKey.privateKey.decodeBase64().aesCBCDecrypt(key, iv).encodeBase64String(), + type = encryptedKey.type.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), + name = encryptedKey.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), + remark = encryptedKey.remark.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), + length = encryptedKey.length, + sort = encryptedKey.sort + ) + SwingUtilities.invokeLater { keyManager.addOhKeyPair(keyPair) } + } catch (e: Exception) { + if (log.isWarnEnabled) { + log.warn("Decode key: ${encryptedKey.id} failed. error: {}", e.message, e) + } + } + } + + if (log.isDebugEnabled) { + log.debug("Decode keys: {}", text) + } + } + + protected fun encodeKeys(key: ByteArray): String { + val encryptedKeys = mutableListOf() + for (keyPair in keyManager.getOhKeyPairs()) { + // aes iv + val iv = ArrayUtils.subarray(keyPair.id.padEnd(16, '0').toByteArray(), 0, 16) + val encryptedKeyPair = OhKeyPair( + id = keyPair.id, + publicKey = keyPair.publicKey.decodeBase64().aesCBCEncrypt(key, iv).encodeBase64String(), + privateKey = keyPair.privateKey.decodeBase64().aesCBCEncrypt(key, iv).encodeBase64String(), + type = keyPair.type.aesCBCEncrypt(key, iv).encodeBase64String(), + name = keyPair.name.aesCBCEncrypt(key, iv).encodeBase64String(), + remark = keyPair.remark.aesCBCEncrypt(key, iv).encodeBase64String(), + length = keyPair.length, + sort = keyPair.sort + ) + encryptedKeys.add(encryptedKeyPair) + } + return ohMyJson.encodeToString(encryptedKeys) + } + + protected fun decodeKeywordHighlights(text: String, config: SyncConfig) { + // aes key + val key = getKey(config) + val encryptedKeywordHighlights = ohMyJson.decodeFromString>(text) + + for (e in encryptedKeywordHighlights) { + try { + // aes iv + val iv = getIv(e.id) + keywordHighlightManager.addKeywordHighlight( + e.copy( + keyword = e.keyword.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), + description = e.description.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), + ) + ) + } catch (ex: Exception) { + if (log.isWarnEnabled) { + log.warn("Decode KeywordHighlight: ${e.id} failed. error: {}", ex.message, ex) + } + } + } + + if (log.isDebugEnabled) { + log.debug("Decode KeywordHighlight: {}", text) + } + } + + protected fun encodeKeywordHighlights(key: ByteArray): String { + val keywordHighlights = mutableListOf() + for (keywordHighlight in keywordHighlightManager.getKeywordHighlights()) { + // aes iv + val iv = getIv(keywordHighlight.id) + val encryptedKeyPair = keywordHighlight.copy( + keyword = keywordHighlight.keyword.aesCBCEncrypt(key, iv).encodeBase64String(), + description = keywordHighlight.description.aesCBCEncrypt(key, iv).encodeBase64String(), + ) + keywordHighlights.add(encryptedKeyPair) + } + return ohMyJson.encodeToString(keywordHighlights) + } + + protected fun decodeMacros(text: String, config: SyncConfig) { + // aes key + val key = getKey(config) + val encryptedMacros = ohMyJson.decodeFromString>(text) + + for (e in encryptedMacros) { + try { + // aes iv + val iv = getIv(e.id) + macroManager.addMacro( + e.copy( + name = e.name.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), + macro = e.macro.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), + ) + ) + } catch (ex: Exception) { + if (log.isWarnEnabled) { + log.warn("Decode Macro: ${e.id} failed. error: {}", ex.message, ex) + } + } + } + + if (log.isDebugEnabled) { + log.debug("Decode Macros: {}", text) + } + } + + protected fun encodeMacros(key: ByteArray): String { + val macros = mutableListOf() + for (macro in macroManager.getMacros()) { + val iv = getIv(macro.id) + macros.add( + macro.copy( + name = macro.name.aesCBCEncrypt(key, iv).encodeBase64String(), + macro = macro.macro.aesCBCEncrypt(key, iv).encodeBase64String() + ) + ) + } + return ohMyJson.encodeToString(macros) + } + + protected fun decodeKeymaps(text: String, config: SyncConfig) { + + for (keymap in ohMyJson.decodeFromString>(text).mapNotNull { Keymap.fromJSON(it) }) { + keymapManager.addKeymap(keymap) + } + + if (log.isDebugEnabled) { + log.debug("Decode Keymaps: {}", text) + } + } + + protected fun encodeKeymaps(): String { + val keymaps = mutableListOf() + for (keymap in keymapManager.getKeymaps()) { + // 只读的是内置的 + if (keymap.isReadonly) { + continue + } + keymaps.add(keymap.toJSONObject()) + } + + return ohMyJson.encodeToString(keymaps) + } + + protected open fun getKey(config: SyncConfig): ByteArray { + return ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16) + } + + protected fun getIv(id: String): ByteArray { + return ArrayUtils.subarray(id.padEnd(16, '0').toByteArray(), 0, 16) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/sync/SyncConfig.kt b/src/main/kotlin/app/termora/sync/SyncConfig.kt index 2f2247b..07ec333 100644 --- a/src/main/kotlin/app/termora/sync/SyncConfig.kt +++ b/src/main/kotlin/app/termora/sync/SyncConfig.kt @@ -4,6 +4,7 @@ enum class SyncType { GitLab, GitHub, Gitee, + WebDAV, } enum class SyncRange { diff --git a/src/main/kotlin/app/termora/sync/SyncerProvider.kt b/src/main/kotlin/app/termora/sync/SyncerProvider.kt index 6565e2a..8bba071 100644 --- a/src/main/kotlin/app/termora/sync/SyncerProvider.kt +++ b/src/main/kotlin/app/termora/sync/SyncerProvider.kt @@ -15,6 +15,7 @@ class SyncerProvider private constructor() { SyncType.GitHub -> GitHubSyncer.getInstance() SyncType.Gitee -> GiteeSyncer.getInstance() SyncType.GitLab -> GitLabSyncer.getInstance() + SyncType.WebDAV -> WebDAVSyncer.getInstance() } } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/sync/WebDAVSyncer.kt b/src/main/kotlin/app/termora/sync/WebDAVSyncer.kt new file mode 100644 index 0000000..0a8d356 --- /dev/null +++ b/src/main/kotlin/app/termora/sync/WebDAVSyncer.kt @@ -0,0 +1,152 @@ +package app.termora.sync + +import app.termora.Application.ohMyJson +import app.termora.ApplicationScope +import app.termora.PBKDF2 +import app.termora.ResponseException +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import okhttp3.Credentials +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.slf4j.LoggerFactory + +class WebDAVSyncer private constructor() : SafetySyncer() { + companion object { + private val log = LoggerFactory.getLogger(WebDAVSyncer::class.java) + + fun getInstance(): WebDAVSyncer { + return ApplicationScope.forApplicationScope().getOrCreate(WebDAVSyncer::class) { WebDAVSyncer() } + } + } + + + override fun pull(config: SyncConfig): GistResponse { + val response = httpClient.newCall(newRequestBuilder(config).get().build()).execute() + if (!response.isSuccessful) { + throw ResponseException(response.code, response) + } + + val text = response.use { resp -> resp.body?.use { it.string() } } + ?: throw ResponseException(response.code, response) + + val json = ohMyJson.decodeFromString(text) + + // decode hosts + json["Hosts"]?.jsonPrimitive?.content?.let { + decodeHosts(it, config) + } + + // decode KeyPairs + json["KeyPairs"]?.jsonPrimitive?.content?.let { + decodeKeys(it, config) + } + + // decode Highlights + json["KeywordHighlights"]?.jsonPrimitive?.content?.let { + decodeKeywordHighlights(it, config) + } + + // decode Macros + json["Macros"]?.jsonPrimitive?.content?.let { + decodeMacros(it, config) + } + + // decode Keymaps + json["Keymaps"]?.jsonPrimitive?.content?.let { + decodeKeymaps(it, config) + } + + return GistResponse(config, emptyList()) + } + + override fun push(config: SyncConfig): GistResponse { + // aes key + val key = getKey(config) + val json = buildJsonObject { + // Hosts + if (config.ranges.contains(SyncRange.Hosts)) { + val hostsContent = encodeHosts(key) + if (log.isDebugEnabled) { + log.debug("Push encryptedHosts: {}", hostsContent) + } + put("Hosts", hostsContent) + } + + // KeyPairs + if (config.ranges.contains(SyncRange.KeyPairs)) { + val keysContent = encodeKeys(key) + if (log.isDebugEnabled) { + log.debug("Push encryptedKeys: {}", keysContent) + } + put("KeyPairs", keysContent) + } + + // Highlights + if (config.ranges.contains(SyncRange.KeywordHighlights)) { + val keywordHighlightsContent = encodeKeywordHighlights(key) + if (log.isDebugEnabled) { + log.debug("Push keywordHighlights: {}", keywordHighlightsContent) + } + put("KeywordHighlights", keywordHighlightsContent) + } + + // Macros + if (config.ranges.contains(SyncRange.Macros)) { + val macrosContent = encodeMacros(key) + if (log.isDebugEnabled) { + log.debug("Push macros: {}", macrosContent) + } + put("Macros", macrosContent) + } + + // Keymap + if (config.ranges.contains(SyncRange.Keymap)) { + val keymapsContent = encodeKeymaps() + if (log.isDebugEnabled) { + log.debug("Push keymaps: {}", keymapsContent) + } + put("Keymaps", keymapsContent) + } + } + + val response = httpClient.newCall( + newRequestBuilder(config).put( + ohMyJson.encodeToString(json) + .toRequestBody("application/json".toMediaType()) + ).build() + ).execute() + + if (!response.isSuccessful) { + throw ResponseException(response.code, response) + } + + return GistResponse( + config = config, + gists = emptyList() + ) + } + + + private fun getWebDavFileUrl(config: SyncConfig): String { + return config.options["domain"] ?: throw IllegalStateException("domain is not defined") + } + + override fun getKey(config: SyncConfig): ByteArray { + return PBKDF2.generateSecret( + config.gistId.toCharArray(), + config.token.toByteArray(), + 10000, 128 + ) + } + + private fun newRequestBuilder(config: SyncConfig): Request.Builder { + return Request.Builder() + .header("Authorization", Credentials.basic(config.gistId, config.token, Charsets.UTF_8)) + .url(getWebDavFileUrl(config)) + } +} \ No newline at end of file diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 9f9827c..44dfbae 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -83,6 +83,7 @@ termora.settings.sync.last-sync-time=Last sync time termora.settings.sync.gist=Gist termora.settings.sync.token=Token termora.settings.sync.type=Type +termora.settings.sync.webdav.help=WebDAV storage address, e.g. https://yourhost/webdav/termora.json termora.settings.about=About termora.settings.about.author=Author diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index 8fc611a..09d6b23 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -85,6 +85,7 @@ termora.settings.sync.import.successful=导入数据成功 termora.settings.sync.gist=片段 termora.settings.sync.token=令牌 termora.settings.sync.type=类型 +termora.settings.sync.webdav.help=WebDAV 的存储地址,例如:https://yourhost/webdav/termora.json termora.settings.about=关于 termora.settings.about.author=作者 diff --git a/src/main/resources/i18n/messages_zh_TW.properties b/src/main/resources/i18n/messages_zh_TW.properties index a68657b..a67de0c 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -89,6 +89,7 @@ termora.settings.sync.import.successful=導入資料成功 termora.settings.sync.gist=片段 termora.settings.sync.token=令牌 termora.settings.sync.type=類型 +termora.settings.sync.webdav.help=WebDAV 的儲存位址,例如:https://yourhost/webdav/termora.json termora.settings.about=關於 termora.settings.about.author=作者