mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-19 01:57:41 +01:00
Compare commits
11 Commits
v2025.9.2
...
v2025.10.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c20c0eff32 | ||
|
|
9d40949043 | ||
|
|
d435337f2a | ||
|
|
a32145c054 | ||
|
|
e0f547b93f | ||
|
|
5d4268d6a1 | ||
|
|
0a3506f81e | ||
|
|
375b2287b7 | ||
|
|
e72c1e68e5 | ||
|
|
3484db3371 | ||
|
|
c4b559f34b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -25,6 +25,7 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
.eslintcache
|
||||
out
|
||||
|
||||
*.sqlite
|
||||
*.sqlite-*
|
||||
|
||||
162
package-lock.json
generated
162
package-lock.json
generated
@@ -61,7 +61,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.7",
|
||||
"@tauri-apps/cli": "^2.9.1",
|
||||
"@tauri-apps/cli": "^2.9.6",
|
||||
"@yaakapp/cli": "^0.3.4",
|
||||
"nodejs-file-downloader": "^4.13.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@@ -3216,9 +3216,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.9.0.tgz",
|
||||
"integrity": "sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw==",
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.9.1.tgz",
|
||||
"integrity": "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -3226,9 +3226,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.9.1.tgz",
|
||||
"integrity": "sha512-kKi2/WWsNXKoMdatBl4xrT7e1Ce27JvsetBVfWuIb6D3ep/Y0WO5SIr70yarXOSWam8NyDur4ipzjZkg6m7VDg==",
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.9.6.tgz",
|
||||
"integrity": "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
@@ -3242,23 +3242,23 @@
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.9.1",
|
||||
"@tauri-apps/cli-darwin-x64": "2.9.1",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.9.1",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.9.1",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.9.1",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.9.1",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.9.1",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.9.1",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.9.1",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.9.1",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.9.1"
|
||||
"@tauri-apps/cli-darwin-arm64": "2.9.6",
|
||||
"@tauri-apps/cli-darwin-x64": "2.9.6",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.9.6",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.9.6",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.9.6",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.9.6",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.9.6",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.9.6",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.9.6",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.9.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.9.1.tgz",
|
||||
"integrity": "sha512-sdwhtsE/6njD0AjgfYEj1JyxZH4SBmCJSXpRm6Ph5fQeuZD6MyjzjdVOrrtFguyREVQ7xn0Ujkwvbo01ULthNg==",
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.9.6.tgz",
|
||||
"integrity": "sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3273,9 +3273,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.9.1.tgz",
|
||||
"integrity": "sha512-c86g+67wTdI4TUCD7CaSd/13+oYuLQxVST4ZNJ5C+6i1kdnU3Us1L68N9MvbDLDQGJc9eo0pvuK6sCWkee+BzA==",
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.9.6.tgz",
|
||||
"integrity": "sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3290,9 +3290,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.9.1.tgz",
|
||||
"integrity": "sha512-IrB3gFQmueQKJjjisOcMktW/Gh6gxgqYO419doA3YZ7yIV5rbE8ZW52Q3I4AO+SlFEyVYer5kpi066p0JBlLGw==",
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.9.6.tgz",
|
||||
"integrity": "sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -3307,9 +3307,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.9.1.tgz",
|
||||
"integrity": "sha512-Ke7TyXvu6HbWSkmVkFbbH19D3cLsd117YtXP/u9NIvSpYwKeFtnbpirrIUfPm44Q+PZFZ2Hvg8X9qoUiAK0zKw==",
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.9.6.tgz",
|
||||
"integrity": "sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3324,9 +3324,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.9.1.tgz",
|
||||
"integrity": "sha512-sGvy75sv55oeMulR5ArwPD28DsDQxqTzLhXCrpU9/nbFg/JImmI7k994YE9fr3V0qE3Cjk5gjLldRNv7I9sjwQ==",
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.9.6.tgz",
|
||||
"integrity": "sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3341,9 +3341,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.9.1.tgz",
|
||||
"integrity": "sha512-tEKbJydV3BdIxpAx8aGHW6VDg1xW4LlQuRD/QeFZdZNTreHJpMbJEcdvAcI+Hg6vgQpVpaoEldR9W4F6dYSLqQ==",
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.9.6.tgz",
|
||||
"integrity": "sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -3358,9 +3358,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.9.1.tgz",
|
||||
"integrity": "sha512-mg5msXHagtHpyCVWgI01M26JeSrgE/otWyGdYcuTwyRYZYEJRTbcNt7hscOkdNlPBe7isScW7PVKbxmAjJJl4g==",
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.9.6.tgz",
|
||||
"integrity": "sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3375,9 +3375,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.9.1.tgz",
|
||||
"integrity": "sha512-lFZEXkpDreUe3zKilvnMsrnKP9gwQudaEjDnOz/GMzbzNceIuPfFZz0cR/ky1Aoq4eSvZonPKHhROq4owz4fzg==",
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.9.6.tgz",
|
||||
"integrity": "sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3392,9 +3392,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.9.1.tgz",
|
||||
"integrity": "sha512-ejc5RAp/Lm1Aj0EQHaT+Wdt5PHfdgQV5hIDV00MV6HNbIb5W4ZUFxMDaRkAg65gl9MvY2fH396riePW3RoKXDw==",
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.9.6.tgz",
|
||||
"integrity": "sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3409,9 +3409,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.9.1.tgz",
|
||||
"integrity": "sha512-fSATtJDc0fNjVB6ystyi8NbwhNFk8i8E05h6KrsC8Fio5eaJIJvPCbC9pdrPl6kkxN1X7fj25ErBbgfqgcK8Fg==",
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.9.6.tgz",
|
||||
"integrity": "sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -3426,9 +3426,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.9.1.tgz",
|
||||
"integrity": "sha512-/JHlOzpUDhjBOO9w167bcYxfJbcMQv7ykS/Y07xjtcga8np0rzUzVGWYmLMH7orKcDMC7wjhheEW1x8cbGma/Q==",
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.9.6.tgz",
|
||||
"integrity": "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3443,63 +3443,63 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-clipboard-manager": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.3.0.tgz",
|
||||
"integrity": "sha512-81NOBA2P+OTY8RLkBwyl9ZR/0CeggLub4F6zxcxUIfFOAqtky7J61+K/MkH2SC1FMxNBxrX0swDuKvkjkHadlA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.3.2.tgz",
|
||||
"integrity": "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.6.0"
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-dialog": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.4.0.tgz",
|
||||
"integrity": "sha512-OvXkrEBfWwtd8tzVCEXIvRfNEX87qs2jv6SqmVPiHcJjBhSF/GUvjqUNIDmKByb5N8nvDqVUM7+g1sXwdC/S9w==",
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.4.2.tgz",
|
||||
"integrity": "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-fs": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.4.2.tgz",
|
||||
"integrity": "sha512-YGhmYuTgXGsi6AjoV+5mh2NvicgWBfVJHHheuck6oHD+HC9bVWPaHvCP0/Aw4pHDejwrvT8hE3+zZAaWf+hrig==",
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.4.4.tgz",
|
||||
"integrity": "sha512-MTorXxIRmOnOPT1jZ3w96vjSuScER38ryXY88vl5F0uiKdnvTKKTtaEjTEo8uPbl4e3gnUtfsDVwC7h77GQLvQ==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-log": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.7.0.tgz",
|
||||
"integrity": "sha512-81XQ2f93x4vmIB5OY0XlYAxy60cHdYLs0Ki8Qp38tNATRiuBit+Orh3frpY3qfYQnqEvYVyRub7YRJWlmW2RRA==",
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.7.1.tgz",
|
||||
"integrity": "sha512-jdb+o0wxQc8PjnLktgGpOs9Dh1YupaOGDXzO+Y8peA1UZ1ep3eXv4E1oiJ7nIQVN0XUFDDhnn3aBszl8ijhR+A==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-opener": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.0.tgz",
|
||||
"integrity": "sha512-B0LShOYae4CZjN8leiNDbnfjSrTwoZakqKaWpfoH6nXiJwt6Rgj6RnVIffG3DoJiKsffRhMkjmBV9VeilSb4TA==",
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.2.tgz",
|
||||
"integrity": "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-os": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.3.1.tgz",
|
||||
"integrity": "sha512-ty5V8XDUIFbSnrk3zsFoP3kzN+vAufYzalJSlmrVhQTImIZa1aL1a03bOaP2vuBvfR+WDRC6NgV2xBl8G07d+w==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.3.2.tgz",
|
||||
"integrity": "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-shell": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.1.tgz",
|
||||
"integrity": "sha512-jjs2WGDO/9z2pjNlydY/F5yYhNsscv99K5lCmU5uKjsVvQ3dRlDhhtVYoa4OLDmktLtQvgvbQjCFibMl6tgGfw==",
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.3.tgz",
|
||||
"integrity": "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
@@ -18687,14 +18687,14 @@
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"@tanstack/react-router": "^1.133.13",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"@tauri-apps/api": "^2.9.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||
"@tauri-apps/plugin-log": "^2.7.0",
|
||||
"@tauri-apps/plugin-opener": "^2.5.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.1",
|
||||
"@tauri-apps/plugin-shell": "^2.3.1",
|
||||
"@tauri-apps/api": "^2.9.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||
"@tauri-apps/plugin-log": "^2.7.1",
|
||||
"@tauri-apps/plugin-opener": "^2.5.2",
|
||||
"@tauri-apps/plugin-os": "^2.3.2",
|
||||
"@tauri-apps/plugin-shell": "^2.3.3",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.5.1",
|
||||
"cm6-graphql": "^0.2.1",
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.7",
|
||||
"@tauri-apps/cli": "^2.9.1",
|
||||
"@tauri-apps/cli": "^2.9.6",
|
||||
"@yaakapp/cli": "^0.3.4",
|
||||
"nodejs-file-downloader": "^4.13.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
|
||||
@@ -11,7 +11,7 @@ async function op(args: CallTemplateFunctionArgs): Promise<{ client?: Client; er
|
||||
let hash: string | null = null;
|
||||
switch (args.values.authMethod) {
|
||||
case 'desktop': {
|
||||
const account = args.values.account;
|
||||
const account = args.values.token;
|
||||
if (typeof account !== 'string' || !account) return { error: 'Missing account name' };
|
||||
|
||||
hash = crypto.createHash('sha256').update(`desktop:${account}`).digest('hex');
|
||||
@@ -97,7 +97,7 @@ export const plugin: PluginDefinition = {
|
||||
name: 'authMethod',
|
||||
type: 'select',
|
||||
label: 'Authentication Method',
|
||||
description: '',
|
||||
defaultValue: 'token',
|
||||
options: [
|
||||
{
|
||||
label: 'Service Account',
|
||||
@@ -108,29 +108,25 @@ export const plugin: PluginDefinition = {
|
||||
value: 'desktop',
|
||||
},
|
||||
],
|
||||
defaultValue: 'token',
|
||||
},
|
||||
{
|
||||
name: 'account',
|
||||
name: 'token',
|
||||
type: 'text',
|
||||
description: '',
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: Yaak template syntax
|
||||
defaultValue: '${[1PASSWORD_TOKEN]}',
|
||||
dynamic(_ctx, args) {
|
||||
switch (args.values.authMethod) {
|
||||
case 'desktop':
|
||||
return {
|
||||
name: 'account',
|
||||
label: 'Account Name',
|
||||
description:
|
||||
'Account name can be taken from the sidebar of the 1Password App. Make sure you\'re on the BETA version of the 1Password app and have "Integrate with other apps" enabled in Settings > Developer.',
|
||||
};
|
||||
case 'token':
|
||||
return {
|
||||
name: 'token',
|
||||
label: 'Token',
|
||||
description:
|
||||
'Token can be generated from the 1Password website by visiting Developer > Service Accounts',
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: Yaak template syntax
|
||||
defaultValue: '${[1PASSWORD_TOKEN]}',
|
||||
password: true,
|
||||
};
|
||||
}
|
||||
|
||||
166
src-tauri/Cargo.lock
generated
166
src-tauri/Cargo.lock
generated
@@ -473,6 +473,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-padding"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.5.1"
|
||||
@@ -700,6 +709,15 @@ dependencies = [
|
||||
"toml 0.8.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.26"
|
||||
@@ -1230,6 +1248,15 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "des"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -2623,6 +2650,7 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"block-padding",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
@@ -3009,9 +3037,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
dependencies = [
|
||||
"value-bag",
|
||||
]
|
||||
@@ -3739,6 +3767,23 @@ dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "p12"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4873306de53fe82e7e484df31e1e947d61514b6ea2ed6cd7b45d63006fd9224"
|
||||
dependencies = [
|
||||
"cbc",
|
||||
"cipher",
|
||||
"des",
|
||||
"getrandom 0.2.16",
|
||||
"hmac",
|
||||
"lazy_static",
|
||||
"rc2",
|
||||
"sha1",
|
||||
"yasna",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
@@ -4489,6 +4534,15 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "rc2"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62c64daa8e9438b84aaae55010a93f396f8e60e3911590fcba770d04643fc1dd"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.12"
|
||||
@@ -4787,6 +4841,15 @@ dependencies = [
|
||||
"security-framework 3.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.12.0"
|
||||
@@ -5620,9 +5683,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.9.2"
|
||||
version = "2.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bceb52453e507c505b330afe3398510e87f428ea42b6e76ecb6bd63b15965b5"
|
||||
checksum = "8a3868da5508446a7cd08956d523ac3edf0a8bc20bf7e4038f9a95c2800d2033"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -5672,9 +5735,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.5.1"
|
||||
version = "2.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a924b6c50fe83193f0f8b14072afa7c25b7a72752a2a73d9549b463f5fe91a38"
|
||||
checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -5694,9 +5757,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.5.0"
|
||||
version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c1fe64c74cc40f90848281a90058a6db931eb400b60205840e09801ee30f190"
|
||||
checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"brotli",
|
||||
@@ -5721,9 +5784,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.5.0"
|
||||
version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "260c5d2eb036b76206b9fca20b7be3614cfd21046c5396f7959e0e64a4b07f2f"
|
||||
checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -5735,9 +5798,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin"
|
||||
version = "2.5.1"
|
||||
version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "076c78a474a7247c90cad0b6e87e593c4c620ed4efdb79cbe0214f0021f6c39d"
|
||||
checksum = "0e1d0a4860b7ff570c891e1d2a586bf1ede205ff858fbc305e0b5ae5d14c1377"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"glob",
|
||||
@@ -5752,9 +5815,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-clipboard-manager"
|
||||
version = "2.3.0"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adddd9e9275b20e77af3061d100a25a884cced3c4c9ef680bd94dd0f7e26c1ca"
|
||||
checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"log",
|
||||
@@ -5767,9 +5830,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "2.4.3"
|
||||
version = "2.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd67112fb1131834c2a7398ffcba520dbbf62c17de3b10329acd1a3554b1a9bb"
|
||||
checksum = "6e82759f7c7d51de3cbde51c04b3f2332de52436ed84541182cd8944b04e9e73"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"plist",
|
||||
@@ -5828,9 +5891,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-log"
|
||||
version = "2.7.0"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c1438bc7662acd16d508c919b3c087efd63669a4c75625dff829b1c75975ec"
|
||||
checksum = "d5709c792b8630290b5d9811a1f8fe983dd925fc87c7fc7f4923616458cd00b6"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"byte-unit",
|
||||
@@ -5850,9 +5913,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.5.0"
|
||||
version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "786156aa8e89e03d271fbd3fe642207da8e65f3c961baa9e2930f332bf80a1f5"
|
||||
checksum = "c26b72571d25dee25667940027114e60f569fc3974f8cefbe50c2cbc5fd65e3b"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"glob",
|
||||
@@ -5872,9 +5935,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-os"
|
||||
version = "2.3.1"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a1c77ebf6f20417ab2a74e8c310820ba52151406d0c80fbcea7df232e3f6ba"
|
||||
checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997"
|
||||
dependencies = [
|
||||
"gethostname 1.0.2",
|
||||
"log",
|
||||
@@ -5911,9 +5974,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-single-instance"
|
||||
version = "2.3.4"
|
||||
version = "2.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb9cac815bf11c4a80fb498666bcdad66d65b89e3ae24669e47806febb76389c"
|
||||
checksum = "dd707f8c86b4e3004e2c141fa24351f1909ba40ce1b8437e30d5ed5277dd3710"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -5959,9 +6022,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-window-state"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d5f6fe3291bfa609c7e0b0ee3bedac294d94c7018934086ce782c1d0f2a468e"
|
||||
checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"log",
|
||||
@@ -5974,9 +6037,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.9.1"
|
||||
version = "2.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9368f09358496f2229313fccb37682ad116b7f46fa76981efe116994a0628926"
|
||||
checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"dpi",
|
||||
@@ -5999,9 +6062,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "2.9.1"
|
||||
version = "2.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "929f5df216f5c02a9e894554401bcdab6eec3e39ec6a4a7731c7067fc8688a93"
|
||||
checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
@@ -6026,9 +6089,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.8.0"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6b8bbe426abdbf52d050e52ed693130dbd68375b9ad82a3fb17efb4c8d85673"
|
||||
checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"brotli",
|
||||
@@ -6810,9 +6873,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "value-bag"
|
||||
version = "1.11.1"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5"
|
||||
checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
@@ -7114,9 +7177,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.0"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01a83f7e1a9f8712695c03eabe9ed3fbca0feff0152f33f12593e5a6303cb1a4"
|
||||
checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
@@ -7855,6 +7918,7 @@ dependencies = [
|
||||
"yaak-sse",
|
||||
"yaak-sync",
|
||||
"yaak-templates",
|
||||
"yaak-tls",
|
||||
"yaak-ws",
|
||||
]
|
||||
|
||||
@@ -7933,12 +7997,13 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin-shell",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tonic",
|
||||
"tonic-reflection",
|
||||
"uuid",
|
||||
"yaak-http",
|
||||
"yaak-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7950,8 +8015,6 @@ dependencies = [
|
||||
"regex",
|
||||
"reqwest",
|
||||
"reqwest_cookie_store",
|
||||
"rustls",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"tauri",
|
||||
"thiserror 2.0.17",
|
||||
@@ -7959,6 +8022,7 @@ dependencies = [
|
||||
"tower-service",
|
||||
"urlencoding",
|
||||
"yaak-models",
|
||||
"yaak-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8093,13 +8157,28 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-tls"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"p12",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"thiserror 2.0.17",
|
||||
"url",
|
||||
"yaak-models",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-ws"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"md5 0.7.0",
|
||||
"md5 0.8.0",
|
||||
"reqwest_cookie_store",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -8112,8 +8191,15 @@ dependencies = [
|
||||
"yaak-models",
|
||||
"yaak-plugins",
|
||||
"yaak-templates",
|
||||
"yaak-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yasna"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.0"
|
||||
|
||||
@@ -12,6 +12,7 @@ members = [
|
||||
"yaak-sse",
|
||||
"yaak-sync",
|
||||
"yaak-templates",
|
||||
"yaak-tls",
|
||||
"yaak-ws",
|
||||
]
|
||||
|
||||
@@ -28,7 +29,9 @@ name = "tauri_app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "lib"]
|
||||
|
||||
[profile.release]
|
||||
strip = true # Automatically strip symbols from the binary.
|
||||
# Currently disabled due to:
|
||||
# Warn Failed to add bundler type to the binary: __TAURI_BUNDLE_TYPE variable not found in binary. Make sure tauri crate and tauri-cli are up to date and that symbol stripping is disabled (https://doc.rust-lang.org/cargo/reference/profiles.html#strip). Updater plugin may not be able to update this package. This shouldn't normally happen, please report it to https://github.com/tauri-apps/tauri/issues
|
||||
strip = false
|
||||
|
||||
[features]
|
||||
cargo-clippy = []
|
||||
@@ -37,7 +40,7 @@ updater = []
|
||||
license = ["yaak-license"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.5.0", features = [] }
|
||||
tauri-build = { version = "2.5.3", features = [] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work
|
||||
@@ -57,17 +60,17 @@ reqwest_cookie_store = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true, features = ["raw_value"] }
|
||||
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
|
||||
tauri-plugin-clipboard-manager = "2.3.0"
|
||||
tauri-plugin-deep-link = "2.4.3"
|
||||
tauri-plugin-clipboard-manager = "2.3.2"
|
||||
tauri-plugin-deep-link = "2.4.5"
|
||||
tauri-plugin-dialog = { workspace = true }
|
||||
tauri-plugin-fs = "2.4.2"
|
||||
tauri-plugin-log = { version = "2.7.0", features = ["colored"] }
|
||||
tauri-plugin-opener = "2.5.0"
|
||||
tauri-plugin-os = "2.3.1"
|
||||
tauri-plugin-fs = "2.4.4"
|
||||
tauri-plugin-log = { version = "2.7.1", features = ["colored"] }
|
||||
tauri-plugin-opener = "2.5.2"
|
||||
tauri-plugin-os = "2.3.2"
|
||||
tauri-plugin-shell = { workspace = true }
|
||||
tauri-plugin-single-instance = { version = "2.3.4", features = ["deep-link"] }
|
||||
tauri-plugin-single-instance = { version = "2.3.6", features = ["deep-link"] }
|
||||
tauri-plugin-updater = "2.9.0"
|
||||
tauri-plugin-window-state = "2.4.0"
|
||||
tauri-plugin-window-state = "2.4.1"
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
tokio-stream = "0.1.17"
|
||||
@@ -86,6 +89,7 @@ yaak-plugins = { workspace = true }
|
||||
yaak-sse = { workspace = true }
|
||||
yaak-sync = { workspace = true }
|
||||
yaak-templates = { workspace = true }
|
||||
yaak-tls = { workspace = true }
|
||||
yaak-ws = { path = "yaak-ws" }
|
||||
|
||||
[workspace.dependencies]
|
||||
@@ -99,9 +103,9 @@ rustls-platform-verifier = "0.6.2"
|
||||
serde = "1.0.228"
|
||||
serde_json = "1.0.145"
|
||||
sha2 = "0.10.9"
|
||||
log = "0.4.28"
|
||||
tauri = "2.9.2"
|
||||
tauri-plugin = "2.5.1"
|
||||
log = "0.4.29"
|
||||
tauri = "2.9.5"
|
||||
tauri-plugin = "2.5.2"
|
||||
tauri-plugin-dialog = "2.4.2"
|
||||
tauri-plugin-shell = "2.3.3"
|
||||
thiserror = "2.0.17"
|
||||
@@ -116,3 +120,4 @@ yaak-plugins = { path = "yaak-plugins" }
|
||||
yaak-sse = { path = "yaak-sse" }
|
||||
yaak-sync = { path = "yaak-sync" }
|
||||
yaak-templates = { path = "yaak-templates" }
|
||||
yaak-tls = { path = "yaak-tls" }
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
|
||||
<!-- Allow loading 1Password's dylib (signed with different Team ID) -->
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
|
||||
<!-- Re-enable for sandboxing. Currently disabled because auto-updater doesn't work with sandboxing.-->
|
||||
<!-- <key>com.apple.security.app-sandbox</key> <true/>-->
|
||||
<!-- <key>com.apple.security.files.user-selected.read-write</key> <true/>-->
|
||||
|
||||
@@ -37,6 +37,7 @@ use yaak_plugins::events::{
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||
use yaak_templates::{RenderErrorBehavior, RenderOptions};
|
||||
use yaak_tls::find_client_certificate;
|
||||
|
||||
pub async fn send_http_request<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
@@ -151,6 +152,8 @@ pub async fn send_http_request_with_context<R: Runtime>(
|
||||
}
|
||||
};
|
||||
|
||||
let client_certificate = find_client_certificate(&url_string, &settings.client_certificates);
|
||||
|
||||
// Add cookie store if specified
|
||||
let maybe_cookie_manager = match cookie_jar.clone() {
|
||||
Some(CookieJar { id, .. }) => {
|
||||
@@ -178,22 +181,19 @@ pub async fn send_http_request_with_context<R: Runtime>(
|
||||
};
|
||||
|
||||
let client = connection_manager
|
||||
.get_client(
|
||||
&plugin_context.id,
|
||||
&HttpConnectionOptions {
|
||||
follow_redirects: workspace.setting_follow_redirects,
|
||||
validate_certificates: workspace.setting_validate_certificates,
|
||||
proxy: proxy_setting,
|
||||
cookie_provider: maybe_cookie_manager.as_ref().map(|(p, _)| Arc::clone(&p)),
|
||||
timeout: if workspace.setting_request_timeout > 0 {
|
||||
Some(Duration::from_millis(
|
||||
workspace.setting_request_timeout.unsigned_abs() as u64
|
||||
))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
.get_client(&HttpConnectionOptions {
|
||||
id: plugin_context.id.clone(),
|
||||
follow_redirects: workspace.setting_follow_redirects,
|
||||
validate_certificates: workspace.setting_validate_certificates,
|
||||
proxy: proxy_setting,
|
||||
cookie_provider: maybe_cookie_manager.as_ref().map(|(p, _)| Arc::clone(&p)),
|
||||
client_certificate,
|
||||
timeout: if workspace.setting_request_timeout > 0 {
|
||||
Some(Duration::from_millis(workspace.setting_request_timeout.unsigned_abs() as u64))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Render query parameters
|
||||
|
||||
@@ -53,6 +53,7 @@ use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||
use yaak_sse::sse::ServerSentEvent;
|
||||
use yaak_templates::format_json::format_json;
|
||||
use yaak_templates::{RenderErrorBehavior, RenderOptions, Tokens, transform_args};
|
||||
use yaak_tls::find_client_certificate;
|
||||
|
||||
mod commands;
|
||||
mod encoding;
|
||||
@@ -187,6 +188,9 @@ async fn cmd_grpc_reflect<R: Runtime>(
|
||||
|
||||
let uri = safe_uri(&req.url);
|
||||
let metadata = build_metadata(&window, &req, &auth_context_id).await?;
|
||||
let settings = window.db().get_settings();
|
||||
let client_certificate =
|
||||
find_client_certificate(req.url.as_str(), &settings.client_certificates);
|
||||
|
||||
Ok(grpc_handle
|
||||
.lock()
|
||||
@@ -197,6 +201,7 @@ async fn cmd_grpc_reflect<R: Runtime>(
|
||||
&proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect(),
|
||||
&metadata,
|
||||
workspace.setting_validate_certificates,
|
||||
client_certificate,
|
||||
skip_cache.unwrap_or(false),
|
||||
)
|
||||
.await
|
||||
@@ -237,6 +242,10 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
|
||||
let metadata = build_metadata(&window, &request, &auth_context_id).await?;
|
||||
|
||||
// Find matching client certificate for this URL
|
||||
let settings = app_handle.db().get_settings();
|
||||
let client_cert = find_client_certificate(&request.url, &settings.client_certificates);
|
||||
|
||||
let conn = app_handle.db().upsert_grpc_connection(
|
||||
&GrpcConnection {
|
||||
workspace_id: request.workspace_id.clone(),
|
||||
@@ -285,6 +294,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
&proto_files.iter().map(|p| PathBuf::from_str(p).unwrap()).collect(),
|
||||
&metadata,
|
||||
workspace.setting_validate_certificates,
|
||||
client_cert.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -294,7 +304,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
app_handle.db().upsert_grpc_connection(
|
||||
&GrpcConnection {
|
||||
elapsed: start.elapsed().as_millis() as i32,
|
||||
error: Some(err.clone()),
|
||||
error: Some(err.to_string()),
|
||||
state: GrpcConnectionState::Closed,
|
||||
..conn.clone()
|
||||
},
|
||||
@@ -425,7 +435,9 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
match (method_desc.is_client_streaming(), method_desc.is_server_streaming()) {
|
||||
(true, true) => (
|
||||
Some(
|
||||
connection.streaming(&service, &method, in_msg_stream, &metadata).await,
|
||||
connection
|
||||
.streaming(&service, &method, in_msg_stream, &metadata, client_cert)
|
||||
.await,
|
||||
),
|
||||
None,
|
||||
),
|
||||
@@ -433,7 +445,13 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
None,
|
||||
Some(
|
||||
connection
|
||||
.client_streaming(&service, &method, in_msg_stream, &metadata)
|
||||
.client_streaming(
|
||||
&service,
|
||||
&method,
|
||||
in_msg_stream,
|
||||
&metadata,
|
||||
client_cert,
|
||||
)
|
||||
.await,
|
||||
),
|
||||
),
|
||||
@@ -441,9 +459,12 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
Some(connection.server_streaming(&service, &method, &msg, &metadata).await),
|
||||
None,
|
||||
),
|
||||
(false, false) => {
|
||||
(None, Some(connection.unary(&service, &method, &msg, &metadata).await))
|
||||
}
|
||||
(false, false) => (
|
||||
None,
|
||||
Some(
|
||||
connection.unary(&service, &method, &msg, &metadata, client_cert).await,
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
if !method_desc.is_client_streaming() {
|
||||
@@ -503,7 +524,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
Some(Err(yaak_grpc::error::Error::GrpcStreamError(e))) => {
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
@@ -528,6 +549,21 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
error: Some(e.to_string()),
|
||||
status: Some(Code::Unknown as i32),
|
||||
content: "Failed to connect".to_string(),
|
||||
event_type: GrpcEventType::ConnectionEnd,
|
||||
..base_event.clone()
|
||||
},
|
||||
&UpdateSource::from_window(&window),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
None => {
|
||||
// Server streaming doesn't return the initial message
|
||||
}
|
||||
@@ -554,7 +590,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
.unwrap();
|
||||
stream.into_inner()
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
Some(Err(yaak_grpc::error::Error::GrpcStreamError(e))) => {
|
||||
warn!("GRPC stream error {e:?}");
|
||||
app_handle
|
||||
.db()
|
||||
@@ -581,6 +617,22 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
.unwrap();
|
||||
return;
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
app_handle
|
||||
.db()
|
||||
.upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
error: Some(e.to_string()),
|
||||
status: Some(Code::Unknown as i32),
|
||||
content: "Failed to connect".to_string(),
|
||||
event_type: GrpcEventType::ConnectionEnd,
|
||||
..base_event.clone()
|
||||
},
|
||||
&UpdateSource::from_window(&window),
|
||||
)
|
||||
.unwrap();
|
||||
return;
|
||||
}
|
||||
None => return,
|
||||
};
|
||||
|
||||
|
||||
@@ -24,4 +24,5 @@ tokio-stream = "0.1.14"
|
||||
tonic = { version = "0.12.3", default-features = false, features = ["transport"] }
|
||||
tonic-reflection = "0.12.3"
|
||||
uuid = { version = "1.7.0", features = ["v4"] }
|
||||
yaak-http = { workspace = true }
|
||||
yaak-tls = { workspace = true }
|
||||
thiserror = "2.0.17"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::error::Result;
|
||||
use crate::manager::decorate_req;
|
||||
use crate::transport::get_transport;
|
||||
use async_recursion::async_recursion;
|
||||
@@ -18,6 +20,7 @@ use tonic_reflection::pb::v1::{
|
||||
};
|
||||
use tonic_reflection::pb::v1::{ExtensionRequest, FileDescriptorResponse};
|
||||
use tonic_reflection::pb::{v1, v1alpha};
|
||||
use yaak_tls::ClientCertificateConfig;
|
||||
|
||||
pub struct AutoReflectionClient<T = Client<HttpsConnector<HttpConnector>, BoxBody>> {
|
||||
use_v1alpha: bool,
|
||||
@@ -26,20 +29,24 @@ pub struct AutoReflectionClient<T = Client<HttpsConnector<HttpConnector>, BoxBod
|
||||
}
|
||||
|
||||
impl AutoReflectionClient {
|
||||
pub fn new(uri: &Uri, validate_certificates: bool) -> Self {
|
||||
pub fn new(
|
||||
uri: &Uri,
|
||||
validate_certificates: bool,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
) -> Result<Self> {
|
||||
let client_v1 = v1::server_reflection_client::ServerReflectionClient::with_origin(
|
||||
get_transport(validate_certificates),
|
||||
get_transport(validate_certificates, client_cert.clone())?,
|
||||
uri.clone(),
|
||||
);
|
||||
let client_v1alpha = v1alpha::server_reflection_client::ServerReflectionClient::with_origin(
|
||||
get_transport(validate_certificates),
|
||||
get_transport(validate_certificates, client_cert.clone())?,
|
||||
uri.clone(),
|
||||
);
|
||||
AutoReflectionClient {
|
||||
Ok(AutoReflectionClient {
|
||||
use_v1alpha: false,
|
||||
client_v1,
|
||||
client_v1alpha,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
@@ -47,36 +54,40 @@ impl AutoReflectionClient {
|
||||
&mut self,
|
||||
message: MessageRequest,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<MessageResponse, String> {
|
||||
) -> Result<MessageResponse> {
|
||||
let reflection_request = ServerReflectionRequest {
|
||||
host: "".into(), // Doesn't matter
|
||||
message_request: Some(message.clone()),
|
||||
};
|
||||
|
||||
if self.use_v1alpha {
|
||||
let mut request = Request::new(tokio_stream::once(to_v1alpha_request(reflection_request)));
|
||||
decorate_req(metadata, &mut request).map_err(|e| e.to_string())?;
|
||||
let mut request =
|
||||
Request::new(tokio_stream::once(to_v1alpha_request(reflection_request)));
|
||||
decorate_req(metadata, &mut request)?;
|
||||
|
||||
self.client_v1alpha
|
||||
.server_reflection_info(request)
|
||||
.await
|
||||
.map_err(|e| match e.code() {
|
||||
tonic::Code::Unavailable => "Failed to connect to endpoint".to_string(),
|
||||
tonic::Code::Unauthenticated => "Authentication failed".to_string(),
|
||||
tonic::Code::DeadlineExceeded => "Deadline exceeded".to_string(),
|
||||
_ => e.to_string(),
|
||||
tonic::Code::Unavailable => {
|
||||
GenericError("Failed to connect to endpoint".to_string())
|
||||
}
|
||||
tonic::Code::Unauthenticated => {
|
||||
GenericError("Authentication failed".to_string())
|
||||
}
|
||||
tonic::Code::DeadlineExceeded => GenericError("Deadline exceeded".to_string()),
|
||||
_ => GenericError(e.to_string()),
|
||||
})?
|
||||
.into_inner()
|
||||
.next()
|
||||
.await
|
||||
.expect("steamed response")
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or(GenericError("Missing reflection message".to_string()))??
|
||||
.message_response
|
||||
.ok_or("No reflection response".to_string())
|
||||
.ok_or(GenericError("No reflection response".to_string()))
|
||||
.map(|resp| to_v1_msg_response(resp))
|
||||
} else {
|
||||
let mut request = Request::new(tokio_stream::once(reflection_request));
|
||||
decorate_req(metadata, &mut request).map_err(|e| e.to_string())?;
|
||||
decorate_req(metadata, &mut request)?;
|
||||
|
||||
let resp = self.client_v1.server_reflection_info(request).await;
|
||||
match resp {
|
||||
@@ -92,18 +103,19 @@ impl AutoReflectionClient {
|
||||
},
|
||||
}
|
||||
.map_err(|e| match e.code() {
|
||||
tonic::Code::Unavailable => "Failed to connect to endpoint".to_string(),
|
||||
tonic::Code::Unauthenticated => "Authentication failed".to_string(),
|
||||
tonic::Code::DeadlineExceeded => "Deadline exceeded".to_string(),
|
||||
_ => e.to_string(),
|
||||
tonic::Code::Unavailable => {
|
||||
GenericError("Failed to connect to endpoint".to_string())
|
||||
}
|
||||
tonic::Code::Unauthenticated => GenericError("Authentication failed".to_string()),
|
||||
tonic::Code::DeadlineExceeded => GenericError("Deadline exceeded".to_string()),
|
||||
_ => GenericError(e.to_string()),
|
||||
})?
|
||||
.into_inner()
|
||||
.next()
|
||||
.await
|
||||
.expect("steamed response")
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or(GenericError("Missing reflection message".to_string()))??
|
||||
.message_response
|
||||
.ok_or("No reflection response".to_string())
|
||||
.ok_or(GenericError("No reflection response".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
51
src-tauri/yaak-grpc/src/error.rs
Normal file
51
src-tauri/yaak-grpc/src/error.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use crate::manager::GrpcStreamError;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde_json::Error as SerdeJsonError;
|
||||
use std::io;
|
||||
use prost::DecodeError;
|
||||
use thiserror::Error;
|
||||
use tonic::Status;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
TlsError(#[from] yaak_tls::error::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
TonicError(#[from] Status),
|
||||
|
||||
#[error("Prost reflect error: {0:?}")]
|
||||
ProstReflectError(#[from] prost_reflect::DescriptorError),
|
||||
|
||||
#[error(transparent)]
|
||||
DeserializerError(#[from] SerdeJsonError),
|
||||
|
||||
#[error(transparent)]
|
||||
GrpcStreamError(#[from] GrpcStreamError),
|
||||
|
||||
#[error(transparent)]
|
||||
GrpcDecodeError(#[from] DecodeError),
|
||||
|
||||
#[error(transparent)]
|
||||
GrpcInvalidMetadataKeyError(#[from] tonic::metadata::errors::InvalidMetadataKey),
|
||||
|
||||
#[error(transparent)]
|
||||
GrpcInvalidMetadataValueError(#[from] tonic::metadata::errors::InvalidMetadataValue),
|
||||
|
||||
#[error(transparent)]
|
||||
IOError(#[from] io::Error),
|
||||
|
||||
#[error("GRPC error: {0}")]
|
||||
GenericError(String),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -9,6 +9,7 @@ pub mod manager;
|
||||
mod reflection;
|
||||
mod transport;
|
||||
mod any;
|
||||
pub mod error;
|
||||
|
||||
pub use tonic::metadata::*;
|
||||
pub use tonic::Code;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::codec::DynamicCodec;
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::error::Result;
|
||||
use crate::reflection::{
|
||||
fill_pool_from_files, fill_pool_from_reflection, method_desc_to_path, reflect_types_for_message,
|
||||
};
|
||||
@@ -12,6 +14,9 @@ pub use prost_reflect::DynamicMessage;
|
||||
use prost_reflect::{DescriptorPool, MethodDescriptor, ServiceDescriptor};
|
||||
use serde_json::Deserializer;
|
||||
use std::collections::BTreeMap;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::fmt::Display;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
@@ -23,6 +28,7 @@ use tonic::body::BoxBody;
|
||||
use tonic::metadata::{MetadataKey, MetadataValue};
|
||||
use tonic::transport::Uri;
|
||||
use tonic::{IntoRequest, IntoStreamingRequest, Request, Response, Status, Streaming};
|
||||
use yaak_tls::ClientCertificateConfig;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GrpcConnection {
|
||||
@@ -33,23 +39,34 @@ pub struct GrpcConnection {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct StreamError {
|
||||
pub struct GrpcStreamError {
|
||||
pub message: String,
|
||||
pub status: Option<Status>,
|
||||
}
|
||||
|
||||
impl From<String> for StreamError {
|
||||
impl Error for GrpcStreamError {}
|
||||
|
||||
impl Display for GrpcStreamError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.status {
|
||||
Some(status) => write!(f, "[{}] {}", status, self.message),
|
||||
None => write!(f, "{}", self.message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for GrpcStreamError {
|
||||
fn from(value: String) -> Self {
|
||||
StreamError {
|
||||
GrpcStreamError {
|
||||
message: value.to_string(),
|
||||
status: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Status> for StreamError {
|
||||
impl From<Status> for GrpcStreamError {
|
||||
fn from(s: Status) -> Self {
|
||||
StreamError {
|
||||
GrpcStreamError {
|
||||
message: s.message().to_string(),
|
||||
status: Some(s),
|
||||
}
|
||||
@@ -57,16 +74,20 @@ impl From<Status> for StreamError {
|
||||
}
|
||||
|
||||
impl GrpcConnection {
|
||||
pub async fn method(&self, service: &str, method: &str) -> Result<MethodDescriptor, String> {
|
||||
pub async fn method(&self, service: &str, method: &str) -> Result<MethodDescriptor> {
|
||||
let service = self.service(service).await?;
|
||||
let method =
|
||||
service.methods().find(|m| m.name() == method).ok_or("Failed to find method")?;
|
||||
let method = service
|
||||
.methods()
|
||||
.find(|m| m.name() == method)
|
||||
.ok_or(GenericError("Failed to find method".to_string()))?;
|
||||
Ok(method)
|
||||
}
|
||||
|
||||
async fn service(&self, service: &str) -> Result<ServiceDescriptor, String> {
|
||||
async fn service(&self, service: &str) -> Result<ServiceDescriptor> {
|
||||
let pool = self.pool.read().await;
|
||||
let service = pool.get_service_by_name(service).ok_or("Failed to find service")?;
|
||||
let service = pool
|
||||
.get_service_by_name(service)
|
||||
.ok_or(GenericError("Failed to find service".to_string()))?;
|
||||
Ok(service)
|
||||
}
|
||||
|
||||
@@ -76,26 +97,27 @@ impl GrpcConnection {
|
||||
method: &str,
|
||||
message: &str,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<Response<DynamicMessage>, StreamError> {
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
) -> Result<Response<DynamicMessage>> {
|
||||
if self.use_reflection {
|
||||
reflect_types_for_message(self.pool.clone(), &self.uri, message, metadata).await?;
|
||||
reflect_types_for_message(self.pool.clone(), &self.uri, message, metadata, client_cert)
|
||||
.await?;
|
||||
}
|
||||
let method = &self.method(&service, &method).await?;
|
||||
let input_message = method.input();
|
||||
|
||||
let mut deserializer = Deserializer::from_str(message);
|
||||
let req_message = DynamicMessage::deserialize(input_message, &mut deserializer)
|
||||
.map_err(|e| e.to_string())?;
|
||||
deserializer.end().unwrap();
|
||||
let req_message = DynamicMessage::deserialize(input_message, &mut deserializer)?;
|
||||
deserializer.end()?;
|
||||
|
||||
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
||||
|
||||
let mut req = req_message.into_request();
|
||||
decorate_req(metadata, &mut req).map_err(|e| e.to_string())?;
|
||||
decorate_req(metadata, &mut req)?;
|
||||
|
||||
let path = method_desc_to_path(method);
|
||||
let codec = DynamicCodec::new(method.clone());
|
||||
client.ready().await.unwrap();
|
||||
client.ready().await.map_err(|e| GenericError(format!("Failed to connect: {}", e)))?;
|
||||
|
||||
Ok(client.unary(req, path, codec).await?)
|
||||
}
|
||||
@@ -106,7 +128,8 @@ impl GrpcConnection {
|
||||
method: &str,
|
||||
stream: ReceiverStream<String>,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<Response<Streaming<DynamicMessage>>, StreamError> {
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
) -> Result<Response<Streaming<DynamicMessage>>> {
|
||||
let method = &self.method(&service, &method).await?;
|
||||
let mapped_stream = {
|
||||
let input_message = method.input();
|
||||
@@ -114,15 +137,19 @@ impl GrpcConnection {
|
||||
let uri = self.uri.clone();
|
||||
let md = metadata.clone();
|
||||
let use_reflection = self.use_reflection.clone();
|
||||
let client_cert = client_cert.clone();
|
||||
stream.filter_map(move |json| {
|
||||
let pool = pool.clone();
|
||||
let uri = uri.clone();
|
||||
let input_message = input_message.clone();
|
||||
let md = md.clone();
|
||||
let use_reflection = use_reflection.clone();
|
||||
let client_cert = client_cert.clone();
|
||||
tauri::async_runtime::block_on(async move {
|
||||
if use_reflection {
|
||||
if let Err(e) = reflect_types_for_message(pool, &uri, &json, &md).await {
|
||||
if let Err(e) =
|
||||
reflect_types_for_message(pool, &uri, &json, &md, client_cert).await
|
||||
{
|
||||
warn!("Failed to resolve Any types: {e}");
|
||||
}
|
||||
}
|
||||
@@ -143,9 +170,9 @@ impl GrpcConnection {
|
||||
let codec = DynamicCodec::new(method.clone());
|
||||
|
||||
let mut req = mapped_stream.into_streaming_request();
|
||||
decorate_req(metadata, &mut req).map_err(|e| e.to_string())?;
|
||||
decorate_req(metadata, &mut req)?;
|
||||
|
||||
client.ready().await.map_err(|e| e.to_string())?;
|
||||
client.ready().await.map_err(|e| GenericError(format!("Failed to connect: {}", e)))?;
|
||||
Ok(client.streaming(req, path, codec).await?)
|
||||
}
|
||||
|
||||
@@ -155,7 +182,8 @@ impl GrpcConnection {
|
||||
method: &str,
|
||||
stream: ReceiverStream<String>,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<Response<DynamicMessage>, StreamError> {
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
) -> Result<Response<DynamicMessage>> {
|
||||
let method = &self.method(&service, &method).await?;
|
||||
let mapped_stream = {
|
||||
let input_message = method.input();
|
||||
@@ -163,15 +191,19 @@ impl GrpcConnection {
|
||||
let uri = self.uri.clone();
|
||||
let md = metadata.clone();
|
||||
let use_reflection = self.use_reflection.clone();
|
||||
let client_cert = client_cert.clone();
|
||||
stream.filter_map(move |json| {
|
||||
let pool = pool.clone();
|
||||
let uri = uri.clone();
|
||||
let input_message = input_message.clone();
|
||||
let md = md.clone();
|
||||
let use_reflection = use_reflection.clone();
|
||||
let client_cert = client_cert.clone();
|
||||
tauri::async_runtime::block_on(async move {
|
||||
if use_reflection {
|
||||
if let Err(e) = reflect_types_for_message(pool, &uri, &json, &md).await {
|
||||
if let Err(e) =
|
||||
reflect_types_for_message(pool, &uri, &json, &md, client_cert).await
|
||||
{
|
||||
warn!("Failed to resolve Any types: {e}");
|
||||
}
|
||||
}
|
||||
@@ -192,13 +224,13 @@ impl GrpcConnection {
|
||||
let codec = DynamicCodec::new(method.clone());
|
||||
|
||||
let mut req = mapped_stream.into_streaming_request();
|
||||
decorate_req(metadata, &mut req).map_err(|e| e.to_string())?;
|
||||
decorate_req(metadata, &mut req)?;
|
||||
|
||||
client.ready().await.unwrap();
|
||||
client.client_streaming(req, path, codec).await.map_err(|e| StreamError {
|
||||
client.ready().await.map_err(|e| GenericError(format!("Failed to connect: {}", e)))?;
|
||||
Ok(client.client_streaming(req, path, codec).await.map_err(|e| GrpcStreamError {
|
||||
message: e.message().to_string(),
|
||||
status: Some(e),
|
||||
})
|
||||
})?)
|
||||
}
|
||||
|
||||
pub async fn server_streaming(
|
||||
@@ -207,23 +239,22 @@ impl GrpcConnection {
|
||||
method: &str,
|
||||
message: &str,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<Response<Streaming<DynamicMessage>>, StreamError> {
|
||||
) -> Result<Response<Streaming<DynamicMessage>>> {
|
||||
let method = &self.method(&service, &method).await?;
|
||||
let input_message = method.input();
|
||||
|
||||
let mut deserializer = Deserializer::from_str(message);
|
||||
let req_message = DynamicMessage::deserialize(input_message, &mut deserializer)
|
||||
.map_err(|e| e.to_string())?;
|
||||
deserializer.end().unwrap();
|
||||
let req_message = DynamicMessage::deserialize(input_message, &mut deserializer)?;
|
||||
deserializer.end()?;
|
||||
|
||||
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
||||
|
||||
let mut req = req_message.into_request();
|
||||
decorate_req(metadata, &mut req).map_err(|e| e.to_string())?;
|
||||
decorate_req(metadata, &mut req)?;
|
||||
|
||||
let path = method_desc_to_path(method);
|
||||
let codec = DynamicCodec::new(method.clone());
|
||||
client.ready().await.map_err(|e| e.to_string())?;
|
||||
client.ready().await.map_err(|e| GenericError(format!("Failed to connect: {}", e)))?;
|
||||
Ok(client.server_streaming(req, path, codec).await?)
|
||||
}
|
||||
}
|
||||
@@ -257,7 +288,8 @@ impl GrpcHandle {
|
||||
proto_files: &Vec<PathBuf>,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
validate_certificates: bool,
|
||||
) -> Result<bool, String> {
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
) -> Result<bool> {
|
||||
let server_reflection = proto_files.is_empty();
|
||||
let key = make_pool_key(id, uri, proto_files);
|
||||
|
||||
@@ -268,7 +300,7 @@ impl GrpcHandle {
|
||||
|
||||
let pool = if server_reflection {
|
||||
let full_uri = uri_from_str(uri)?;
|
||||
fill_pool_from_reflection(&full_uri, metadata, validate_certificates).await
|
||||
fill_pool_from_reflection(&full_uri, metadata, validate_certificates, client_cert).await
|
||||
} else {
|
||||
fill_pool_from_files(&self.app_handle, proto_files).await
|
||||
}?;
|
||||
@@ -284,15 +316,19 @@ impl GrpcHandle {
|
||||
proto_files: &Vec<PathBuf>,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
validate_certificates: bool,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
skip_cache: bool,
|
||||
) -> Result<Vec<ServiceDefinition>, String> {
|
||||
) -> Result<Vec<ServiceDefinition>> {
|
||||
// Ensure we have a pool; reflect only if missing
|
||||
if skip_cache || self.get_pool(id, uri, proto_files).is_none() {
|
||||
info!("Reflecting gRPC services for {} at {}", id, uri);
|
||||
self.reflect(id, uri, proto_files, metadata, validate_certificates).await?;
|
||||
self.reflect(id, uri, proto_files, metadata, validate_certificates, client_cert)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let pool = self.get_pool(id, uri, proto_files).ok_or("Failed to get pool".to_string())?;
|
||||
let pool = self
|
||||
.get_pool(id, uri, proto_files)
|
||||
.ok_or(GenericError("Failed to get pool".to_string()))?;
|
||||
Ok(self.services_from_pool(&pool))
|
||||
}
|
||||
|
||||
@@ -313,7 +349,7 @@ impl GrpcHandle {
|
||||
&pool,
|
||||
input_message,
|
||||
))
|
||||
.unwrap(),
|
||||
.expect("Failed to serialize JSON schema"),
|
||||
})
|
||||
}
|
||||
def
|
||||
@@ -328,14 +364,26 @@ impl GrpcHandle {
|
||||
proto_files: &Vec<PathBuf>,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
validate_certificates: bool,
|
||||
) -> Result<GrpcConnection, String> {
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
) -> Result<GrpcConnection> {
|
||||
let use_reflection = proto_files.is_empty();
|
||||
if self.get_pool(id, uri, proto_files).is_none() {
|
||||
self.reflect(id, uri, proto_files, metadata, validate_certificates).await?;
|
||||
self.reflect(
|
||||
id,
|
||||
uri,
|
||||
proto_files,
|
||||
metadata,
|
||||
validate_certificates,
|
||||
client_cert.clone(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let pool = self.get_pool(id, uri, proto_files).ok_or("Failed to get pool")?.clone();
|
||||
let pool = self
|
||||
.get_pool(id, uri, proto_files)
|
||||
.ok_or(GenericError("Failed to get pool".to_string()))?
|
||||
.clone();
|
||||
let uri = uri_from_str(uri)?;
|
||||
let conn = get_transport(validate_certificates);
|
||||
let conn = get_transport(validate_certificates, client_cert.clone())?;
|
||||
Ok(GrpcConnection {
|
||||
pool: Arc::new(RwLock::new(pool)),
|
||||
use_reflection,
|
||||
@@ -352,22 +400,20 @@ impl GrpcHandle {
|
||||
pub(crate) fn decorate_req<T>(
|
||||
metadata: &BTreeMap<String, String>,
|
||||
req: &mut Request<T>,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<()> {
|
||||
for (k, v) in metadata {
|
||||
req.metadata_mut().insert(
|
||||
MetadataKey::from_str(k.as_str()).map_err(|e| e.to_string())?,
|
||||
MetadataValue::from_str(v.as_str()).map_err(|e| e.to_string())?,
|
||||
);
|
||||
req.metadata_mut()
|
||||
.insert(MetadataKey::from_str(k.as_str())?, MetadataValue::from_str(v.as_str())?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn uri_from_str(uri_str: &str) -> Result<Uri, String> {
|
||||
fn uri_from_str(uri_str: &str) -> Result<Uri> {
|
||||
match Uri::from_str(uri_str) {
|
||||
Ok(uri) => Ok(uri),
|
||||
Err(err) => {
|
||||
// Uri::from_str basically only returns "invalid format" so we add more context here
|
||||
Err(format!("Failed to parse URL, {}", err.to_string()))
|
||||
Err(GenericError(format!("Failed to parse URL, {}", err.to_string())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::any::collect_any_types;
|
||||
use crate::client::AutoReflectionClient;
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::error::Result;
|
||||
use anyhow::anyhow;
|
||||
use async_recursion::async_recursion;
|
||||
use log::{debug, info, warn};
|
||||
@@ -21,11 +23,12 @@ use tonic::codegen::http::uri::PathAndQuery;
|
||||
use tonic::transport::Uri;
|
||||
use tonic_reflection::pb::v1::server_reflection_request::MessageRequest;
|
||||
use tonic_reflection::pb::v1::server_reflection_response::MessageResponse;
|
||||
use yaak_tls::ClientCertificateConfig;
|
||||
|
||||
pub async fn fill_pool_from_files(
|
||||
app_handle: &AppHandle,
|
||||
paths: &Vec<PathBuf>,
|
||||
) -> Result<DescriptorPool, String> {
|
||||
) -> Result<DescriptorPool> {
|
||||
let mut pool = DescriptorPool::new();
|
||||
let random_file_name = format!("{}.desc", uuid::Uuid::new_v4());
|
||||
let desc_path = temp_dir().join(random_file_name);
|
||||
@@ -103,18 +106,18 @@ pub async fn fill_pool_from_files(
|
||||
.expect("yaakprotoc failed to run");
|
||||
|
||||
if !out.status.success() {
|
||||
return Err(format!(
|
||||
return Err(GenericError(format!(
|
||||
"protoc failed with status {}: {}",
|
||||
out.status.code().unwrap(),
|
||||
String::from_utf8_lossy(out.stderr.as_slice())
|
||||
));
|
||||
)));
|
||||
}
|
||||
|
||||
let bytes = fs::read(desc_path).await.map_err(|e| e.to_string())?;
|
||||
let fdp = FileDescriptorSet::decode(bytes.deref()).map_err(|e| e.to_string())?;
|
||||
pool.add_file_descriptor_set(fdp).map_err(|e| e.to_string())?;
|
||||
let bytes = fs::read(desc_path).await?;
|
||||
let fdp = FileDescriptorSet::decode(bytes.deref())?;
|
||||
pool.add_file_descriptor_set(fdp)?;
|
||||
|
||||
fs::remove_file(desc_path).await.map_err(|e| e.to_string())?;
|
||||
fs::remove_file(desc_path).await?;
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
@@ -123,9 +126,10 @@ pub async fn fill_pool_from_reflection(
|
||||
uri: &Uri,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
validate_certificates: bool,
|
||||
) -> Result<DescriptorPool, String> {
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
) -> Result<DescriptorPool> {
|
||||
let mut pool = DescriptorPool::new();
|
||||
let mut client = AutoReflectionClient::new(uri, validate_certificates);
|
||||
let mut client = AutoReflectionClient::new(uri, validate_certificates, client_cert)?;
|
||||
|
||||
for service in list_services(&mut client, metadata).await? {
|
||||
if service == "grpc.reflection.v1alpha.ServerReflection" {
|
||||
@@ -144,7 +148,7 @@ pub async fn fill_pool_from_reflection(
|
||||
async fn list_services(
|
||||
client: &mut AutoReflectionClient,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
) -> Result<Vec<String>> {
|
||||
let response =
|
||||
client.send_reflection_request(MessageRequest::ListServices("".into()), metadata).await?;
|
||||
|
||||
@@ -171,7 +175,7 @@ async fn file_descriptor_set_from_service_name(
|
||||
{
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
warn!("Error fetching file descriptor for service {}: {}", service_name, e);
|
||||
warn!("Error fetching file descriptor for service {}: {:?}", service_name, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -195,7 +199,8 @@ pub(crate) async fn reflect_types_for_message(
|
||||
uri: &Uri,
|
||||
json: &str,
|
||||
metadata: &BTreeMap<String, String>,
|
||||
) -> Result<(), String> {
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
) -> Result<()> {
|
||||
// 1. Collect all Any types in the JSON
|
||||
let mut extra_types = Vec::new();
|
||||
collect_any_types(json, &mut extra_types);
|
||||
@@ -204,7 +209,7 @@ pub(crate) async fn reflect_types_for_message(
|
||||
return Ok(()); // nothing to do
|
||||
}
|
||||
|
||||
let mut client = AutoReflectionClient::new(uri, false);
|
||||
let mut client = AutoReflectionClient::new(uri, false, client_cert)?;
|
||||
for extra_type in extra_types {
|
||||
{
|
||||
let guard = pool.read().await;
|
||||
@@ -217,9 +222,9 @@ pub(crate) async fn reflect_types_for_message(
|
||||
let resp = match client.send_reflection_request(req, metadata).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Err(format!(
|
||||
"Error sending reflection request for @type \"{extra_type}\": {e}",
|
||||
));
|
||||
return Err(GenericError(format!(
|
||||
"Error sending reflection request for @type \"{extra_type}\": {e:?}",
|
||||
)));
|
||||
}
|
||||
};
|
||||
let files = match resp {
|
||||
@@ -286,7 +291,7 @@ async fn file_descriptor_set_by_filename(
|
||||
panic!("Expected a FileDescriptorResponse variant")
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error fetching file descriptor for {}: {}", filename, e);
|
||||
warn!("Error fetching file descriptor for {}: {:?}", filename, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,25 +1,41 @@
|
||||
use crate::error::Result;
|
||||
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
|
||||
use hyper_util::client::legacy::connect::HttpConnector;
|
||||
use hyper_util::client::legacy::Client;
|
||||
use hyper_util::rt::TokioExecutor;
|
||||
use log::info;
|
||||
use tonic::body::BoxBody;
|
||||
use yaak_tls::{get_tls_config, ClientCertificateConfig};
|
||||
|
||||
// I think ALPN breaks this because we're specifying http2_only
|
||||
const WITH_ALPN: bool = false;
|
||||
|
||||
pub(crate) fn get_transport(validate_certificates: bool) -> Client<HttpsConnector<HttpConnector>, BoxBody> {
|
||||
let tls_config = yaak_http::tls::get_config(validate_certificates, WITH_ALPN);
|
||||
pub(crate) fn get_transport(
|
||||
validate_certificates: bool,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
) -> Result<Client<HttpsConnector<HttpConnector>, BoxBody>> {
|
||||
let tls_config =
|
||||
get_tls_config(validate_certificates, WITH_ALPN, client_cert.clone())?;
|
||||
|
||||
let mut http = HttpConnector::new();
|
||||
http.enforce_http(false);
|
||||
|
||||
let connector =
|
||||
HttpsConnectorBuilder::new().with_tls_config(tls_config).https_or_http().enable_http2().build();
|
||||
let connector = HttpsConnectorBuilder::new()
|
||||
.with_tls_config(tls_config)
|
||||
.https_or_http()
|
||||
.enable_http2()
|
||||
.build();
|
||||
|
||||
let client = Client::builder(TokioExecutor::new())
|
||||
.pool_max_idle_per_host(0)
|
||||
.http2_only(true)
|
||||
.build(connector);
|
||||
|
||||
client
|
||||
info!(
|
||||
"Created gRPC client validate_certs={} client_cert={}",
|
||||
validate_certificates,
|
||||
client_cert.is_some()
|
||||
);
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
@@ -5,17 +5,16 @@ edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
yaak-models = { workspace = true }
|
||||
hyper-util = { version = "0.1.17", default-features = false, features = ["client-legacy"] }
|
||||
log = { workspace = true }
|
||||
regex = "1.11.1"
|
||||
rustls = { workspace = true, default-features = false, features = ["ring"] }
|
||||
rustls-platform-verifier = { workspace = true }
|
||||
urlencoding = "2.1.3"
|
||||
tauri = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider", "socks", "http2"] }
|
||||
reqwest_cookie_store = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
hyper-util = { version = "0.1.17", default-features = false, features = ["client-legacy"] }
|
||||
tauri = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tower-service = "0.3.3"
|
||||
log = { workspace = true }
|
||||
urlencoding = "2.1.3"
|
||||
yaak-models = { workspace = true }
|
||||
yaak-tls = { workspace = true }
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::dns::LocalhostResolver;
|
||||
use crate::error::Result;
|
||||
use crate::tls;
|
||||
use log::{debug, warn};
|
||||
use log::{debug, info, warn};
|
||||
use reqwest::redirect::Policy;
|
||||
use reqwest::{Client, Proxy};
|
||||
use reqwest_cookie_store::CookieStoreMutex;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use yaak_tls::{ClientCertificateConfig, get_tls_config};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HttpConnectionProxySettingAuth {
|
||||
@@ -28,11 +28,13 @@ pub enum HttpConnectionProxySetting {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HttpConnectionOptions {
|
||||
pub id: String,
|
||||
pub follow_redirects: bool,
|
||||
pub validate_certificates: bool,
|
||||
pub proxy: HttpConnectionProxySetting,
|
||||
pub cookie_provider: Option<Arc<CookieStoreMutex>>,
|
||||
pub timeout: Option<Duration>,
|
||||
pub client_certificate: Option<ClientCertificateConfig>,
|
||||
}
|
||||
|
||||
impl HttpConnectionOptions {
|
||||
@@ -45,8 +47,10 @@ impl HttpConnectionOptions {
|
||||
.referer(false)
|
||||
.tls_info(true);
|
||||
|
||||
// Configure TLS
|
||||
client = client.use_preconfigured_tls(tls::get_config(self.validate_certificates, true));
|
||||
// Configure TLS with optional client certificate
|
||||
let config =
|
||||
get_tls_config(self.validate_certificates, true, self.client_certificate.clone())?;
|
||||
client = client.use_preconfigured_tls(config);
|
||||
|
||||
// Configure DNS resolver
|
||||
client = client.dns_resolver(LocalhostResolver::new());
|
||||
@@ -85,6 +89,12 @@ impl HttpConnectionOptions {
|
||||
client = client.timeout(d);
|
||||
}
|
||||
|
||||
info!(
|
||||
"Building new HTTP client validate_certificates={} client_cert={}",
|
||||
self.validate_certificates,
|
||||
self.client_certificate.is_some()
|
||||
);
|
||||
|
||||
Ok(client.build()?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@ use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
#[error("Client error: {0:?}")]
|
||||
Client(#[from] reqwest::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
TlsError(#[from] yaak_tls::error::Error),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
|
||||
@@ -7,7 +7,6 @@ pub mod dns;
|
||||
pub mod error;
|
||||
pub mod manager;
|
||||
pub mod path_placeholders;
|
||||
pub mod tls;
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("yaak-http")
|
||||
|
||||
@@ -20,19 +20,19 @@ impl HttpConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_client(&self, id: &str, opt: &HttpConnectionOptions) -> Result<Client> {
|
||||
pub async fn get_client(&self, opt: &HttpConnectionOptions) -> Result<Client> {
|
||||
let mut connections = self.connections.write().await;
|
||||
let id = opt.id.clone();
|
||||
|
||||
// Clean old connections
|
||||
connections.retain(|_, (_, last_used)| last_used.elapsed() <= self.ttl);
|
||||
|
||||
if let Some((c, last_used)) = connections.get_mut(id) {
|
||||
if let Some((c, last_used)) = connections.get_mut(&id) {
|
||||
info!("Re-using HTTP client {id}");
|
||||
*last_used = Instant::now();
|
||||
return Ok(c.clone());
|
||||
}
|
||||
|
||||
info!("Building new HTTP client {id}");
|
||||
let c = opt.build_client()?;
|
||||
connections.insert(id.into(), (c.clone(), Instant::now()));
|
||||
Ok(c)
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
|
||||
use rustls::crypto::ring;
|
||||
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
|
||||
use rustls_platform_verifier::BuilderVerifierExt;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn get_config(validate_certificates: bool, with_alpn: bool) -> ClientConfig {
|
||||
let arc_crypto_provider = Arc::new(ring::default_provider());
|
||||
let config_builder = ClientConfig::builder_with_provider(arc_crypto_provider)
|
||||
.with_safe_default_protocol_versions()
|
||||
.unwrap();
|
||||
let mut client = if validate_certificates {
|
||||
// Use platform-native verifier to validate certificates
|
||||
config_builder.with_platform_verifier().unwrap().with_no_client_auth()
|
||||
} else {
|
||||
config_builder
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoVerifier))
|
||||
.with_no_client_auth()
|
||||
};
|
||||
|
||||
if with_alpn {
|
||||
client.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
||||
}
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
// Copied from reqwest: https://github.com/seanmonstar/reqwest/blob/595c80b1fbcdab73ac2ae93e4edc3406f453df25/src/tls.rs#L608
|
||||
#[derive(Debug)]
|
||||
struct NoVerifier;
|
||||
|
||||
impl ServerCertVerifier for NoVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &CertificateDer,
|
||||
_intermediates: &[CertificateDer],
|
||||
_server_name: &ServerName,
|
||||
_ocsp_response: &[u8],
|
||||
_now: UnixTime,
|
||||
) -> Result<ServerCertVerified, rustls::Error> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
vec![
|
||||
SignatureScheme::RSA_PKCS1_SHA1,
|
||||
SignatureScheme::ECDSA_SHA1_Legacy,
|
||||
SignatureScheme::RSA_PKCS1_SHA256,
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
SignatureScheme::RSA_PKCS1_SHA384,
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
SignatureScheme::RSA_PKCS1_SHA512,
|
||||
SignatureScheme::ECDSA_NISTP521_SHA512,
|
||||
SignatureScheme::RSA_PSS_SHA256,
|
||||
SignatureScheme::RSA_PSS_SHA384,
|
||||
SignatureScheme::RSA_PSS_SHA512,
|
||||
SignatureScheme::ED25519,
|
||||
SignatureScheme::ED448,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ mod mac;
|
||||
|
||||
use crate::commands::{set_theme, set_title};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use tauri::{generate_handler, plugin, plugin::TauriPlugin, Manager, Runtime};
|
||||
use tauri::{Manager, Runtime, generate_handler, plugin, plugin::TauriPlugin};
|
||||
|
||||
pub trait AppHandleMacWindowExt {
|
||||
/// Sets whether to use the native titlebar
|
||||
@@ -14,7 +14,9 @@ pub trait AppHandleMacWindowExt {
|
||||
|
||||
impl<R: Runtime> AppHandleMacWindowExt for tauri::AppHandle<R> {
|
||||
fn set_native_titlebar(&self, enable: bool) {
|
||||
self.state::<PluginState>().native_titlebar.store(enable, std::sync::atomic::Ordering::Relaxed);
|
||||
self.state::<PluginState>()
|
||||
.native_titlebar
|
||||
.store(enable, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,17 +25,21 @@ pub(crate) struct PluginState {
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
plugin::Builder::new("yaak-mac-window")
|
||||
let mut builder = plugin::Builder::new("yaak-mac-window")
|
||||
.setup(move |app, _| {
|
||||
app.manage(PluginState { native_titlebar: AtomicBool::new(false) });
|
||||
app.manage(PluginState {
|
||||
native_titlebar: AtomicBool::new(false),
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(generate_handler![set_title, set_theme])
|
||||
.on_window_ready(move |window| {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
mac::setup_traffic_light_positioner(&window);
|
||||
}
|
||||
})
|
||||
.build()
|
||||
.invoke_handler(generate_handler![set_title, set_theme]);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
builder = builder.on_window_ready(move |window| {
|
||||
mac::setup_traffic_light_positioner(&window);
|
||||
});
|
||||
}
|
||||
|
||||
builder.build()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
export type AnyModel = CookieJar | Environment | Folder | GraphQlIntrospection | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | KeyValue | Plugin | Settings | SyncState | WebsocketConnection | WebsocketEvent | WebsocketRequest | Workspace | WorkspaceMeta;
|
||||
|
||||
export type ClientCertificate = { host: string, port: number | null, crtFile: string | null, keyFile: string | null, pfxFile: string | null, passphrase: string | null, enabled?: boolean, };
|
||||
|
||||
export type Cookie = { raw_cookie: string, domain: CookieDomain, expires: CookieExpires, path: [string, boolean], };
|
||||
|
||||
export type CookieDomain = { "HostOnly": string } | { "Suffix": string } | "NotPresent" | "Empty";
|
||||
@@ -62,7 +64,7 @@ export type ProxySetting = { "type": "enabled", http: string, https: string, aut
|
||||
|
||||
export type ProxySettingAuth = { user: string, password: string, };
|
||||
|
||||
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, useNativeTitlebar: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, hideLicenseBadge: boolean, autoupdate: boolean, autoDownloadUpdates: boolean, checkNotifications: boolean, };
|
||||
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, clientCertificates: Array<ClientCertificate>, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, useNativeTitlebar: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, hideLicenseBadge: boolean, autoupdate: boolean, autoDownloadUpdates: boolean, checkNotifications: boolean, };
|
||||
|
||||
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE settings ADD COLUMN client_certificates TEXT DEFAULT '[]' NOT NULL;
|
||||
@@ -52,6 +52,26 @@ pub struct ProxySettingAuth {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub struct ClientCertificate {
|
||||
pub host: String,
|
||||
#[serde(default)]
|
||||
pub port: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub crt_file: Option<String>,
|
||||
#[serde(default)]
|
||||
pub key_file: Option<String>,
|
||||
#[serde(default)]
|
||||
pub pfx_file: Option<String>,
|
||||
#[serde(default)]
|
||||
pub passphrase: Option<String>,
|
||||
#[serde(default = "default_true")]
|
||||
#[ts(optional, as = "Option<bool>")]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
@@ -106,6 +126,7 @@ pub struct Settings {
|
||||
pub updated_at: NaiveDateTime,
|
||||
|
||||
pub appearance: String,
|
||||
pub client_certificates: Vec<ClientCertificate>,
|
||||
pub colored_methods: bool,
|
||||
pub editor_font: Option<String>,
|
||||
pub editor_font_size: i32,
|
||||
@@ -158,10 +179,12 @@ impl UpsertModelInfo for Settings {
|
||||
None => None,
|
||||
Some(p) => Some(serde_json::to_string(&p)?),
|
||||
};
|
||||
let client_certificates = serde_json::to_string(&self.client_certificates)?;
|
||||
Ok(vec![
|
||||
(CreatedAt, upsert_date(source, self.created_at)),
|
||||
(UpdatedAt, upsert_date(source, self.updated_at)),
|
||||
(Appearance, self.appearance.as_str().into()),
|
||||
(ClientCertificates, client_certificates.into()),
|
||||
(EditorFontSize, self.editor_font_size.into()),
|
||||
(EditorKeymap, self.editor_keymap.to_string().into()),
|
||||
(EditorSoftWrap, self.editor_soft_wrap.into()),
|
||||
@@ -188,6 +211,7 @@ impl UpsertModelInfo for Settings {
|
||||
vec![
|
||||
SettingsIden::UpdatedAt,
|
||||
SettingsIden::Appearance,
|
||||
SettingsIden::ClientCertificates,
|
||||
SettingsIden::EditorFontSize,
|
||||
SettingsIden::EditorKeymap,
|
||||
SettingsIden::EditorSoftWrap,
|
||||
@@ -215,6 +239,7 @@ impl UpsertModelInfo for Settings {
|
||||
Self: Sized,
|
||||
{
|
||||
let proxy: Option<String> = row.get("proxy")?;
|
||||
let client_certificates: String = row.get("client_certificates")?;
|
||||
let editor_keymap: String = row.get("editor_keymap")?;
|
||||
Ok(Self {
|
||||
id: row.get("id")?,
|
||||
@@ -222,6 +247,7 @@ impl UpsertModelInfo for Settings {
|
||||
created_at: row.get("created_at")?,
|
||||
updated_at: row.get("updated_at")?,
|
||||
appearance: row.get("appearance")?,
|
||||
client_certificates: serde_json::from_str(&client_certificates).unwrap_or_default(),
|
||||
editor_font_size: row.get("editor_font_size")?,
|
||||
editor_font: row.get("editor_font")?,
|
||||
editor_keymap: EditorKeymap::from_str(editor_keymap.as_str()).unwrap(),
|
||||
|
||||
@@ -18,6 +18,7 @@ impl<'a> DbContext<'a> {
|
||||
updated_at: Default::default(),
|
||||
|
||||
appearance: "system".to_string(),
|
||||
client_certificates: Vec::new(),
|
||||
editor_font_size: 12,
|
||||
editor_font: None,
|
||||
editor_keymap: EditorKeymap::Default,
|
||||
|
||||
16
src-tauri/yaak-tls/Cargo.toml
Normal file
16
src-tauri/yaak-tls/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "yaak-tls"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
p12 = "0.6.3"
|
||||
rustls = { workspace = true, default-features = false, features = ["ring"] }
|
||||
rustls-pemfile = "2"
|
||||
rustls-platform-verifier = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
thiserror = "2.0.17"
|
||||
url = "2.5"
|
||||
yaak-models = { workspace = true }
|
||||
26
src-tauri/yaak-tls/src/error.rs
Normal file
26
src-tauri/yaak-tls/src/error.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Rustls error: {0}")]
|
||||
RustlsError(#[from] rustls::Error),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
IOError(#[from] io::Error),
|
||||
|
||||
#[error("TLS error: {0}")]
|
||||
GenericError(String),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
279
src-tauri/yaak-tls/src/lib.rs
Normal file
279
src-tauri/yaak-tls/src/lib.rs
Normal file
@@ -0,0 +1,279 @@
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::error::Result;
|
||||
use log::debug;
|
||||
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
|
||||
use rustls::crypto::ring;
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime};
|
||||
use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
|
||||
use rustls_platform_verifier::BuilderVerifierExt;
|
||||
use std::fs;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod error;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ClientCertificateConfig {
|
||||
pub crt_file: Option<String>,
|
||||
pub key_file: Option<String>,
|
||||
pub pfx_file: Option<String>,
|
||||
pub passphrase: Option<String>,
|
||||
}
|
||||
|
||||
pub fn get_tls_config(
|
||||
validate_certificates: bool,
|
||||
with_alpn: bool,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
) -> Result<ClientConfig> {
|
||||
let maybe_client_cert = load_client_cert(client_cert)?;
|
||||
|
||||
let mut client = if validate_certificates {
|
||||
build_with_validation(maybe_client_cert)
|
||||
} else {
|
||||
build_without_validation(maybe_client_cert)
|
||||
}?;
|
||||
|
||||
if with_alpn {
|
||||
client.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
||||
}
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
fn build_with_validation(
|
||||
client_cert: Option<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>,
|
||||
) -> Result<ClientConfig> {
|
||||
let arc_crypto_provider = Arc::new(ring::default_provider());
|
||||
let builder = ClientConfig::builder_with_provider(arc_crypto_provider)
|
||||
.with_safe_default_protocol_versions()?
|
||||
.with_platform_verifier()?;
|
||||
|
||||
if let Some((certs, key)) = client_cert {
|
||||
return Ok(builder.with_client_auth_cert(certs, key)?);
|
||||
}
|
||||
|
||||
Ok(builder.with_no_client_auth())
|
||||
}
|
||||
|
||||
fn build_without_validation(
|
||||
client_cert: Option<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>,
|
||||
) -> Result<ClientConfig> {
|
||||
let arc_crypto_provider = Arc::new(ring::default_provider());
|
||||
let builder = ClientConfig::builder_with_provider(arc_crypto_provider)
|
||||
.with_safe_default_protocol_versions()?
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(NoVerifier));
|
||||
|
||||
if let Some((certs, key)) = client_cert {
|
||||
return Ok(builder.with_client_auth_cert(certs, key)?);
|
||||
}
|
||||
|
||||
Ok(builder.with_no_client_auth())
|
||||
}
|
||||
|
||||
fn load_client_cert(
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
) -> Result<Option<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>> {
|
||||
let config = match client_cert {
|
||||
None => return Ok(None),
|
||||
Some(c) => c,
|
||||
};
|
||||
|
||||
// Try PFX/PKCS12 first
|
||||
if let Some(pfx_path) = &config.pfx_file {
|
||||
if !pfx_path.is_empty() {
|
||||
return Ok(Some(load_pkcs12(pfx_path, config.passphrase.as_deref().unwrap_or(""))?));
|
||||
}
|
||||
}
|
||||
|
||||
// Try CRT + KEY files
|
||||
if let (Some(crt_path), Some(key_path)) = (&config.crt_file, &config.key_file) {
|
||||
if !crt_path.is_empty() && !key_path.is_empty() {
|
||||
return Ok(Some(load_pem_files(crt_path, key_path)?));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn load_pem_files(
|
||||
crt_path: &str,
|
||||
key_path: &str,
|
||||
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
|
||||
// Load certificates
|
||||
let crt_file = fs::File::open(Path::new(crt_path))?;
|
||||
let mut crt_reader = BufReader::new(crt_file);
|
||||
let certs: Vec<CertificateDer<'static>> =
|
||||
rustls_pemfile::certs(&mut crt_reader).filter_map(|r| r.ok()).collect();
|
||||
|
||||
if certs.is_empty() {
|
||||
return Err(GenericError("No certificates found in CRT file".to_string()));
|
||||
}
|
||||
|
||||
// Load private key
|
||||
let key_data = fs::read(Path::new(key_path))?;
|
||||
let key = load_private_key(&key_data)?;
|
||||
|
||||
Ok((certs, key))
|
||||
}
|
||||
|
||||
fn load_private_key(data: &[u8]) -> Result<PrivateKeyDer<'static>> {
|
||||
let mut reader = BufReader::new(data);
|
||||
|
||||
// Try PKCS8 first
|
||||
if let Some(key) = rustls_pemfile::pkcs8_private_keys(&mut reader).filter_map(|r| r.ok()).next()
|
||||
{
|
||||
return Ok(PrivateKeyDer::Pkcs8(key));
|
||||
}
|
||||
|
||||
// Reset reader and try RSA
|
||||
let mut reader = BufReader::new(data);
|
||||
if let Some(key) = rustls_pemfile::rsa_private_keys(&mut reader).filter_map(|r| r.ok()).next() {
|
||||
return Ok(PrivateKeyDer::Pkcs1(key));
|
||||
}
|
||||
|
||||
// Reset reader and try EC
|
||||
let mut reader = BufReader::new(data);
|
||||
if let Some(key) = rustls_pemfile::ec_private_keys(&mut reader).filter_map(|r| r.ok()).next() {
|
||||
return Ok(PrivateKeyDer::Sec1(key));
|
||||
}
|
||||
|
||||
Err(GenericError("Could not parse private key".to_string()))
|
||||
}
|
||||
|
||||
fn load_pkcs12(
|
||||
path: &str,
|
||||
passphrase: &str,
|
||||
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
|
||||
let data = fs::read(Path::new(path))?;
|
||||
|
||||
let pfx = p12::PFX::parse(&data)
|
||||
.map_err(|e| GenericError(format!("Failed to parse PFX: {:?}", e)))?;
|
||||
|
||||
let keys = pfx
|
||||
.key_bags(passphrase)
|
||||
.map_err(|e| GenericError(format!("Failed to extract keys: {:?}", e)))?;
|
||||
|
||||
let certs = pfx
|
||||
.cert_x509_bags(passphrase)
|
||||
.map_err(|e| GenericError(format!("Failed to extract certs: {:?}", e)))?;
|
||||
|
||||
if keys.is_empty() {
|
||||
return Err(GenericError("No private key found in PFX".to_string()));
|
||||
}
|
||||
|
||||
if certs.is_empty() {
|
||||
return Err(GenericError("No certificates found in PFX".to_string()));
|
||||
}
|
||||
|
||||
// Convert certificates - p12 crate returns Vec<u8> for each cert
|
||||
let cert_ders: Vec<CertificateDer<'static>> =
|
||||
certs.into_iter().map(|c| CertificateDer::from(c)).collect();
|
||||
|
||||
// Convert key - the p12 crate returns raw key bytes
|
||||
let key_bytes = keys.into_iter().next().unwrap();
|
||||
let key = PrivateKeyDer::Pkcs8(key_bytes.into());
|
||||
|
||||
Ok((cert_ders, key))
|
||||
}
|
||||
|
||||
// Copied from reqwest: https://github.com/seanmonstar/reqwest/blob/595c80b1fbcdab73ac2ae93e4edc3406f453df25/src/tls.rs#L608
|
||||
#[derive(Debug)]
|
||||
struct NoVerifier;
|
||||
|
||||
impl ServerCertVerifier for NoVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &CertificateDer,
|
||||
_intermediates: &[CertificateDer],
|
||||
_server_name: &ServerName,
|
||||
_ocsp_response: &[u8],
|
||||
_now: UnixTime,
|
||||
) -> std::result::Result<ServerCertVerified, rustls::Error> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
vec![
|
||||
SignatureScheme::RSA_PKCS1_SHA1,
|
||||
SignatureScheme::ECDSA_SHA1_Legacy,
|
||||
SignatureScheme::RSA_PKCS1_SHA256,
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
SignatureScheme::RSA_PKCS1_SHA384,
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
SignatureScheme::RSA_PKCS1_SHA512,
|
||||
SignatureScheme::ECDSA_NISTP521_SHA512,
|
||||
SignatureScheme::RSA_PSS_SHA256,
|
||||
SignatureScheme::RSA_PSS_SHA384,
|
||||
SignatureScheme::RSA_PSS_SHA512,
|
||||
SignatureScheme::ED25519,
|
||||
SignatureScheme::ED448,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_client_certificate(
|
||||
url_string: &str,
|
||||
certificates: &[yaak_models::models::ClientCertificate],
|
||||
) -> Option<ClientCertificateConfig> {
|
||||
let url = url::Url::from_str(url_string).ok()?;
|
||||
let host = url.host_str()?;
|
||||
let port = url.port_or_known_default();
|
||||
|
||||
for cert in certificates {
|
||||
if !cert.enabled {
|
||||
debug!("Client certificate is disabled, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match host (case-insensitive)
|
||||
if !cert.host.eq_ignore_ascii_case(host) {
|
||||
debug!("Client certificate host does not match {} != {} (cert)", host, cert.host);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match port if specified in the certificate config
|
||||
let cert_port = cert.port.unwrap_or(443);
|
||||
if let Some(url_port) = port {
|
||||
if cert_port != url_port as i32 {
|
||||
debug!(
|
||||
"Client certificate port does not match {} != {} (cert)",
|
||||
url_port, cert_port
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Found a matching certificate
|
||||
debug!("Found matching client certificate host={} port={}", host, port.unwrap_or(443));
|
||||
return Some(ClientCertificateConfig {
|
||||
crt_file: cert.crt_file.clone(),
|
||||
key_file: cert.key_file.clone(),
|
||||
pfx_file: cert.pfx_file.clone(),
|
||||
passphrase: cert.passphrase.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
debug!("No matching client certificate found for {}", url_string);
|
||||
None
|
||||
}
|
||||
@@ -8,7 +8,7 @@ publish = false
|
||||
[dependencies]
|
||||
futures-util = "0.3.31"
|
||||
log = { workspace = true }
|
||||
md5 = "0.7.0"
|
||||
md5 = "0.8.0"
|
||||
reqwest_cookie_store = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
@@ -17,6 +17,7 @@ thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "time", "test-util"] }
|
||||
tokio-tungstenite = { version = "0.26.2", default-features = false, features = ["rustls-tls-native-roots", "connect"] }
|
||||
yaak-http = { workspace = true }
|
||||
yaak-tls = { workspace = true }
|
||||
yaak-models = { workspace = true }
|
||||
yaak-plugins = { workspace = true }
|
||||
yaak-templates = { workspace = true }
|
||||
|
||||
@@ -23,6 +23,7 @@ use yaak_plugins::events::{
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||
use yaak_templates::{RenderErrorBehavior, RenderOptions};
|
||||
use yaak_tls::find_client_certificate;
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn upsert_request<R: Runtime>(
|
||||
@@ -196,6 +197,7 @@ pub(crate) async fn connect<R: Runtime>(
|
||||
environment_id,
|
||||
)?;
|
||||
let workspace = app_handle.db().get_workspace(&unrendered_request.workspace_id)?;
|
||||
let settings = app_handle.db().get_settings();
|
||||
let (resolved_request, auth_context_id) =
|
||||
resolve_websocket_request(&window, &unrendered_request)?;
|
||||
let request = render_websocket_request(
|
||||
@@ -363,6 +365,8 @@ pub(crate) async fn connect<R: Runtime>(
|
||||
}
|
||||
}
|
||||
|
||||
let client_cert = find_client_certificate(url.as_str(), &settings.client_certificates);
|
||||
|
||||
let response = match ws_manager
|
||||
.connect(
|
||||
&connection.id,
|
||||
@@ -370,6 +374,7 @@ pub(crate) async fn connect<R: Runtime>(
|
||||
headers,
|
||||
receive_tx,
|
||||
workspace.setting_validate_certificates,
|
||||
client_cert,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::error::Result;
|
||||
use log::info;
|
||||
use std::sync::Arc;
|
||||
use tauri::http::HeaderMap;
|
||||
@@ -9,6 +10,7 @@ use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
|
||||
use tokio_tungstenite::{
|
||||
Connector, MaybeTlsStream, WebSocketStream, connect_async_tls_with_config,
|
||||
};
|
||||
use yaak_tls::{ClientCertificateConfig, get_tls_config};
|
||||
|
||||
// Enabling ALPN breaks websocket requests
|
||||
const WITH_ALPN: bool = false;
|
||||
@@ -17,9 +19,10 @@ pub(crate) async fn ws_connect(
|
||||
url: &str,
|
||||
headers: HeaderMap<HeaderValue>,
|
||||
validate_certificates: bool,
|
||||
) -> crate::error::Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response)> {
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response)> {
|
||||
info!("Connecting to WS {url}");
|
||||
let tls_config = yaak_http::tls::get_config(validate_certificates, WITH_ALPN);
|
||||
let tls_config = get_tls_config(validate_certificates, WITH_ALPN, client_cert.clone())?;
|
||||
|
||||
let mut req = url.into_client_request()?;
|
||||
let req_headers = req.headers_mut();
|
||||
@@ -36,5 +39,12 @@ pub(crate) async fn ws_connect(
|
||||
Some(Connector::Rustls(Arc::new(tls_config))),
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!(
|
||||
"Connected to WS {url} validate_certificates={} client_cert={}",
|
||||
validate_certificates,
|
||||
client_cert.is_some()
|
||||
);
|
||||
|
||||
Ok((stream, response))
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
TemplateError(#[from] yaak_templates::error::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
TlsError(#[from] yaak_tls::error::Error),
|
||||
|
||||
#[error("WebSocket error: {0}")]
|
||||
GenericError(String),
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::tungstenite::handshake::client::Response;
|
||||
use tokio_tungstenite::tungstenite::http::{HeaderMap, HeaderValue};
|
||||
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
||||
use yaak_tls::ClientCertificateConfig;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WebsocketManager {
|
||||
@@ -35,10 +36,12 @@ impl WebsocketManager {
|
||||
headers: HeaderMap<HeaderValue>,
|
||||
receive_tx: mpsc::Sender<Message>,
|
||||
validate_certificates: bool,
|
||||
client_cert: Option<ClientCertificateConfig>,
|
||||
) -> Result<Response> {
|
||||
let tx = receive_tx.clone();
|
||||
|
||||
let (stream, response) = ws_connect(url, headers, validate_certificates).await?;
|
||||
let (stream, response) =
|
||||
ws_connect(url, headers, validate_certificates, client_cert).await?;
|
||||
let (write, mut read) = stream.split();
|
||||
|
||||
self.connections.lock().await.insert(id.to_string(), write);
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { useSearch } from '@tanstack/react-router';
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
import { useLicense } from '@yaakapp-internal/license';
|
||||
import { pluginsAtom, settingsAtom } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useState } from 'react';
|
||||
import { useKeyPressEvent } from 'react-use';
|
||||
import { appInfo } from '../../lib/appInfo';
|
||||
import { capitalize } from '../../lib/capitalize';
|
||||
import { CountBadge } from '../core/CountBadge';
|
||||
import { HStack } from '../core/Stacks';
|
||||
import type { TabItem } from '../core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from '../core/Tabs/Tabs';
|
||||
import { TabContent, type TabItem, Tabs } from '../core/Tabs/Tabs';
|
||||
import { HeaderSize } from '../HeaderSize';
|
||||
import { SettingsCertificates } from './SettingsCertificates';
|
||||
import { SettingsGeneral } from './SettingsGeneral';
|
||||
import { SettingsInterface } from './SettingsInterface';
|
||||
import { SettingsLicense } from './SettingsLicense';
|
||||
@@ -25,14 +29,26 @@ const TAB_GENERAL = 'general';
|
||||
const TAB_INTERFACE = 'interface';
|
||||
const TAB_THEME = 'theme';
|
||||
const TAB_PROXY = 'proxy';
|
||||
const TAB_CERTIFICATES = 'certificates';
|
||||
const TAB_PLUGINS = 'plugins';
|
||||
const TAB_LICENSE = 'license';
|
||||
const tabs = [TAB_GENERAL, TAB_THEME, TAB_INTERFACE, TAB_PROXY, TAB_PLUGINS, TAB_LICENSE] as const;
|
||||
const tabs = [
|
||||
TAB_GENERAL,
|
||||
TAB_THEME,
|
||||
TAB_INTERFACE,
|
||||
TAB_CERTIFICATES,
|
||||
TAB_PROXY,
|
||||
TAB_PLUGINS,
|
||||
TAB_LICENSE,
|
||||
] as const;
|
||||
export type SettingsTab = (typeof tabs)[number];
|
||||
|
||||
export default function Settings({ hide }: Props) {
|
||||
const { tab: tabFromQuery } = useSearch({ from: '/workspaces/$workspaceId/settings' });
|
||||
const [tab, setTab] = useState<string | undefined>(tabFromQuery);
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const plugins = useAtomValue(pluginsAtom);
|
||||
const licenseCheck = useLicense();
|
||||
|
||||
// Close settings window on escape
|
||||
// TODO: Could this be put in a better place? Eg. in Rust key listener when creating the window
|
||||
@@ -79,6 +95,16 @@ export default function Settings({ hide }: Props) {
|
||||
value,
|
||||
label: capitalize(value),
|
||||
hidden: !appInfo.featureLicense && value === TAB_LICENSE,
|
||||
rightSlot:
|
||||
value === TAB_CERTIFICATES ? (
|
||||
<CountBadge count={settings.clientCertificates.length} />
|
||||
) : value === TAB_PLUGINS ? (
|
||||
<CountBadge count={plugins.length} />
|
||||
) : value === TAB_PROXY && settings.proxy?.type === 'enabled' ? (
|
||||
<CountBadge count />
|
||||
) : value === TAB_LICENSE && licenseCheck.check.data?.status === 'personal_use' ? (
|
||||
<CountBadge count color="notice" />
|
||||
) : null,
|
||||
}),
|
||||
)}
|
||||
>
|
||||
@@ -97,6 +123,9 @@ export default function Settings({ hide }: Props) {
|
||||
<TabContent value={TAB_PROXY} className="overflow-y-auto h-full px-6 !py-4">
|
||||
<SettingsProxy />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_CERTIFICATES} className="overflow-y-auto h-full px-6 !py-4">
|
||||
<SettingsCertificates />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_LICENSE} className="overflow-y-auto h-full px-6 !py-4">
|
||||
<SettingsLicense />
|
||||
</TabContent>
|
||||
|
||||
253
src-web/components/Settings/SettingsCertificates.tsx
Normal file
253
src-web/components/Settings/SettingsCertificates.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import type { ClientCertificate } from '@yaakapp-internal/models';
|
||||
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useRef } from 'react';
|
||||
import { showConfirmDelete } from '../../lib/confirm';
|
||||
import { Button } from '../core/Button';
|
||||
import { Checkbox } from '../core/Checkbox';
|
||||
import { DetailsBanner } from '../core/DetailsBanner';
|
||||
import { Heading } from '../core/Heading';
|
||||
import { IconButton } from '../core/IconButton';
|
||||
import { InlineCode } from '../core/InlineCode';
|
||||
import { PlainInput } from '../core/PlainInput';
|
||||
import { Separator } from '../core/Separator';
|
||||
import { HStack, VStack } from '../core/Stacks';
|
||||
import { SelectFile } from '../SelectFile';
|
||||
|
||||
function createEmptyCertificate(): ClientCertificate {
|
||||
return {
|
||||
host: '',
|
||||
port: null,
|
||||
crtFile: null,
|
||||
keyFile: null,
|
||||
pfxFile: null,
|
||||
passphrase: null,
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
interface CertificateEditorProps {
|
||||
certificate: ClientCertificate;
|
||||
index: number;
|
||||
onUpdate: (index: number, cert: ClientCertificate) => void;
|
||||
onRemove: (index: number) => void;
|
||||
}
|
||||
|
||||
function CertificateEditor({ certificate, index, onUpdate, onRemove }: CertificateEditorProps) {
|
||||
const updateField = <K extends keyof ClientCertificate>(
|
||||
field: K,
|
||||
value: ClientCertificate[K],
|
||||
) => {
|
||||
onUpdate(index, { ...certificate, [field]: value });
|
||||
};
|
||||
|
||||
const hasPfx = Boolean(certificate.pfxFile && certificate.pfxFile.length > 0);
|
||||
const hasCrtKey = Boolean(
|
||||
(certificate.crtFile && certificate.crtFile.length > 0) ||
|
||||
(certificate.keyFile && certificate.keyFile.length > 0),
|
||||
);
|
||||
|
||||
// Determine certificate type for display
|
||||
const certType = hasPfx ? 'PFX' : hasCrtKey ? 'CERT' : null;
|
||||
const defaultOpen = useRef<boolean>(!certificate.host);
|
||||
|
||||
return (
|
||||
<DetailsBanner
|
||||
open={defaultOpen.current}
|
||||
summary={
|
||||
<HStack alignItems="center" justifyContent="between" space={2} className="w-full">
|
||||
<HStack space={1.5}>
|
||||
<Checkbox
|
||||
className="ml-1"
|
||||
checked={certificate.enabled ?? true}
|
||||
title={certificate.enabled ? 'Disable certificate' : 'Enable certificate'}
|
||||
hideLabel
|
||||
onChange={(enabled) => updateField('enabled', enabled)}
|
||||
/>
|
||||
|
||||
{certificate.host ? (
|
||||
<InlineCode>
|
||||
{certificate.host || <> </>}
|
||||
{certificate.port != null && `:${certificate.port}`}
|
||||
</InlineCode>
|
||||
) : (
|
||||
<span className="italic text-sm text-text-subtlest">Configure Certificate</span>
|
||||
)}
|
||||
{certType && <InlineCode>{certType}</InlineCode>}
|
||||
</HStack>
|
||||
<IconButton
|
||||
icon="trash"
|
||||
size="sm"
|
||||
title="Remove certificate"
|
||||
className="text-text-subtlest -mr-2"
|
||||
onClick={() => onRemove(index)}
|
||||
/>
|
||||
</HStack>
|
||||
}
|
||||
>
|
||||
<VStack space={3} className="mt-2">
|
||||
<HStack space={2} alignItems="end">
|
||||
<PlainInput
|
||||
leftSlot={
|
||||
<div className="bg-surface-highlight flex items-center text-editor font-mono px-2 text-text-subtle mr-1">
|
||||
https://
|
||||
</div>
|
||||
}
|
||||
validate={(value) => {
|
||||
if (!value) return false;
|
||||
if (!/^[a-zA-Z0-9_.-]+$/.test(value)) return false;
|
||||
return true;
|
||||
}}
|
||||
label="Host"
|
||||
placeholder="example.com"
|
||||
size="sm"
|
||||
required
|
||||
defaultValue={certificate.host}
|
||||
onChange={(host) => updateField('host', host)}
|
||||
/>
|
||||
<PlainInput
|
||||
label="Port"
|
||||
hideLabel
|
||||
validate={(value) => {
|
||||
if (!value) return true;
|
||||
if (Number.isNaN(parseInt(value, 10))) return false;
|
||||
return true;
|
||||
}}
|
||||
placeholder="443"
|
||||
leftSlot={
|
||||
<div className="bg-surface-highlight flex items-center text-editor font-mono px-2 text-text-subtle mr-1">
|
||||
:
|
||||
</div>
|
||||
}
|
||||
size="sm"
|
||||
className="w-24"
|
||||
defaultValue={certificate.port?.toString() ?? ''}
|
||||
onChange={(port) => updateField('port', port ? parseInt(port, 10) : null)}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
<Separator className="my-3" />
|
||||
|
||||
<VStack space={2}>
|
||||
<SelectFile
|
||||
label="CRT File"
|
||||
noun="Cert"
|
||||
filePath={certificate.crtFile ?? null}
|
||||
size="sm"
|
||||
disabled={hasPfx}
|
||||
onChange={({ filePath }) => updateField('crtFile', filePath)}
|
||||
/>
|
||||
<SelectFile
|
||||
label="KEY File"
|
||||
noun="Key"
|
||||
filePath={certificate.keyFile ?? null}
|
||||
size="sm"
|
||||
disabled={hasPfx}
|
||||
onChange={({ filePath }) => updateField('keyFile', filePath)}
|
||||
/>
|
||||
</VStack>
|
||||
|
||||
<Separator className="my-3" />
|
||||
|
||||
<SelectFile
|
||||
label="PFX File"
|
||||
noun="Key"
|
||||
filePath={certificate.pfxFile ?? null}
|
||||
size="sm"
|
||||
disabled={hasCrtKey}
|
||||
onChange={({ filePath }) => updateField('pfxFile', filePath)}
|
||||
/>
|
||||
|
||||
<PlainInput
|
||||
label="Passphrase"
|
||||
size="sm"
|
||||
type="password"
|
||||
defaultValue={certificate.passphrase ?? ''}
|
||||
onChange={(passphrase) => updateField('passphrase', passphrase || null)}
|
||||
/>
|
||||
</VStack>
|
||||
</DetailsBanner>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsCertificates() {
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const certificates = settings.clientCertificates ?? [];
|
||||
|
||||
const updateCertificates = async (newCertificates: ClientCertificate[]) => {
|
||||
await patchModel(settings, { clientCertificates: newCertificates });
|
||||
};
|
||||
|
||||
const handleAdd = async () => {
|
||||
const newCert = createEmptyCertificate();
|
||||
await updateCertificates([...certificates, newCert]);
|
||||
};
|
||||
|
||||
const handleUpdate = async (index: number, cert: ClientCertificate) => {
|
||||
const newCertificates = [...certificates];
|
||||
newCertificates[index] = cert;
|
||||
await updateCertificates(newCertificates);
|
||||
};
|
||||
|
||||
const handleRemove = async (index: number) => {
|
||||
const cert = certificates[index];
|
||||
if (cert == null) return;
|
||||
|
||||
const host = cert.host || 'this certificate';
|
||||
const port = cert.port != null ? `:${cert.port}` : '';
|
||||
|
||||
const confirmed = await showConfirmDelete({
|
||||
id: 'confirm-remove-certificate',
|
||||
title: 'Delete Certificate',
|
||||
description: (
|
||||
<>
|
||||
Permanently delete certificate for{' '}
|
||||
<InlineCode>
|
||||
{host}
|
||||
{port}
|
||||
</InlineCode>
|
||||
?
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
const newCertificates = certificates.filter((_, i) => i !== index);
|
||||
|
||||
await updateCertificates(newCertificates);
|
||||
};
|
||||
|
||||
return (
|
||||
<VStack space={3}>
|
||||
<div className="mb-3">
|
||||
<HStack justifyContent="between" alignItems="start">
|
||||
<div>
|
||||
<Heading>Client Certificates</Heading>
|
||||
<p className="text-text-subtle">
|
||||
Add and manage TLS certificates on a per domain basis
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="border" size="sm" color="secondary" onClick={handleAdd}>
|
||||
Add Certificate
|
||||
</Button>
|
||||
</HStack>
|
||||
</div>
|
||||
|
||||
{certificates.length > 0 && (
|
||||
<VStack space={3}>
|
||||
{certificates.map((cert, index) => (
|
||||
<CertificateEditor
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Index is fine here
|
||||
key={index}
|
||||
certificate={cert}
|
||||
index={index}
|
||||
onUpdate={handleUpdate}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,10 @@ export function SettingsGeneral() {
|
||||
|
||||
return (
|
||||
<VStack space={1.5} className="mb-4">
|
||||
<div className="mb-4">
|
||||
<Heading>General</Heading>
|
||||
<p className="text-text-subtle">Configure general settings for update behavior and more.</p>
|
||||
</div>
|
||||
<CargoFeature feature="updater">
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-1">
|
||||
<Select
|
||||
|
||||
@@ -13,6 +13,7 @@ import { invokeCmd } from '../../lib/tauri';
|
||||
import { CargoFeature } from '../CargoFeature';
|
||||
import { Button } from '../core/Button';
|
||||
import { Checkbox } from '../core/Checkbox';
|
||||
import { Heading } from '../core/Heading';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { Link } from '../core/Link';
|
||||
import { Select } from '../core/Select';
|
||||
@@ -42,6 +43,10 @@ export function SettingsInterface() {
|
||||
|
||||
return (
|
||||
<VStack space={3} className="mb-4">
|
||||
<div className="mb-3">
|
||||
<Heading>Interface</Heading>
|
||||
<p className="text-text-subtle">Tweak settings related to the user interface.</p>
|
||||
</div>
|
||||
<Select
|
||||
name="switchWorkspaceBehavior"
|
||||
label="Open workspace behavior"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { patchModel, settingsAtom } from '@yaakapp-internal/models';
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
import { Checkbox } from '../core/Checkbox';
|
||||
import { Heading } from '../core/Heading';
|
||||
import { InlineCode } from '../core/InlineCode';
|
||||
import { PlainInput } from '../core/PlainInput';
|
||||
import { Select } from '../core/Select';
|
||||
@@ -13,6 +14,13 @@ export function SettingsProxy() {
|
||||
|
||||
return (
|
||||
<VStack space={1.5} className="mb-4">
|
||||
<div className="mb-3">
|
||||
<Heading>Proxy</Heading>
|
||||
<p className="text-text-subtle">
|
||||
Configure a proxy server for HTTP requests. Useful for corporate firewalls, debugging
|
||||
traffic, or routing through specific infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
<Select
|
||||
name="proxy"
|
||||
label="Proxy"
|
||||
|
||||
@@ -5,9 +5,11 @@ import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
|
||||
import { useResolvedAppearance } from '../../hooks/useResolvedAppearance';
|
||||
import { useResolvedTheme } from '../../hooks/useResolvedTheme';
|
||||
import type { ButtonProps } from '../core/Button';
|
||||
import { Heading } from '../core/Heading';
|
||||
import type { IconProps } from '../core/Icon';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { IconButton } from '../core/IconButton';
|
||||
import { Link } from '../core/Link';
|
||||
import type { SelectProps } from '../core/Select';
|
||||
import { Select } from '../core/Select';
|
||||
import { HStack, VStack } from '../core/Stacks';
|
||||
@@ -69,6 +71,15 @@ export function SettingsTheme() {
|
||||
|
||||
return (
|
||||
<VStack space={3} className="mb-4">
|
||||
<div className="mb-3">
|
||||
<Heading>Theme</Heading>
|
||||
<p className="text-text-subtle">
|
||||
Make Yaak your own by selecting a theme, or{' '}
|
||||
<Link href="https://feedback.yaak.app/help/articles/6911763-plugins-quick-start">
|
||||
Create Your Own
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<Select
|
||||
name="appearance"
|
||||
label="Appearance"
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { Color } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
count: number | true;
|
||||
className?: string;
|
||||
color?: Color;
|
||||
}
|
||||
|
||||
export function CountBadge({ count, className }: Props) {
|
||||
export function CountBadge({ count, className, color }: Props) {
|
||||
if (count === 0) return null;
|
||||
return (
|
||||
<div
|
||||
@@ -13,10 +15,21 @@ export function CountBadge({ count, className }: Props) {
|
||||
className={classNames(
|
||||
className,
|
||||
'flex items-center',
|
||||
'opacity-70 border border-border-subtle text-4xs rounded mb-0.5 px-1 ml-1 h-4 font-mono',
|
||||
'opacity-70 border text-4xs rounded mb-0.5 px-1 ml-1 h-4 font-mono',
|
||||
color == null && 'border-border-subtle',
|
||||
color === 'primary' && 'text-primary',
|
||||
color === 'secondary' && 'text-secondary',
|
||||
color === 'success' && 'text-success',
|
||||
color === 'notice' && 'text-notice',
|
||||
color === 'warning' && 'text-warning',
|
||||
color === 'danger' && 'text-danger',
|
||||
)}
|
||||
>
|
||||
{count === true ? <div aria-hidden className="rounded-full h-1 w-1 bg-text-subtle" /> : count}
|
||||
{count === true ? (
|
||||
<div aria-hidden className="rounded-full h-1 w-1 bg-[currentColor]" />
|
||||
) : (
|
||||
count
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function DetailsBanner({ className, color, summary, children, ...extraPro
|
||||
/>
|
||||
{summary}
|
||||
</summary>
|
||||
<div className="mt-1.5">{children}</div>
|
||||
<div className="mt-1.5 pb-2">{children}</div>
|
||||
</details>
|
||||
</Banner>
|
||||
);
|
||||
|
||||
@@ -162,6 +162,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
||||
'x-theme-input',
|
||||
'relative w-full rounded-md text',
|
||||
'border',
|
||||
'overflow-hidden',
|
||||
focused ? 'border-border-focus' : 'border-border-subtle',
|
||||
hasChanged && 'has-[:invalid]:border-danger', // For built-in HTML validation
|
||||
size === 'md' && 'min-h-md',
|
||||
|
||||
@@ -27,14 +27,14 @@
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"@tanstack/react-router": "^1.133.13",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"@tauri-apps/api": "^2.9.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||
"@tauri-apps/plugin-log": "^2.7.0",
|
||||
"@tauri-apps/plugin-opener": "^2.5.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.1",
|
||||
"@tauri-apps/plugin-shell": "^2.3.1",
|
||||
"@tauri-apps/api": "^2.9.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||
"@tauri-apps/plugin-log": "^2.7.1",
|
||||
"@tauri-apps/plugin-opener": "^2.5.2",
|
||||
"@tauri-apps/plugin-os": "^2.3.2",
|
||||
"@tauri-apps/plugin-shell": "^2.3.3",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.5.1",
|
||||
"cm6-graphql": "^0.2.1",
|
||||
|
||||
Reference in New Issue
Block a user