Initial commit.
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/target
|
||||
|
||||
|
||||
# Added by cargo
|
||||
#
|
||||
# already existing elements were commented out
|
||||
|
||||
#/target
|
||||
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
11
.idea/gemini-grs.iml
generated
Normal file
11
.idea/gemini-grs.iml
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="EMPTY_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/gemini-grs.iml" filepath="$PROJECT_DIR$/.idea/gemini-grs.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
551
Cargo.lock
generated
Normal file
551
Cargo.lock
generated
Normal file
@@ -0,0 +1,551 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"humantime",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gemini-grs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"env_logger",
|
||||
"hex",
|
||||
"log",
|
||||
"nanoid",
|
||||
"openssl",
|
||||
"path-clean",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.156"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5f43f184355eefb8d17fc948dbecf6c13be3c141f20d834ae842193a448c72a"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "nanoid"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
|
||||
dependencies = [
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "path-clean"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "gemini-grs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
env_logger = "0.11.5"
|
||||
hex = "0.4.3"
|
||||
log = "0.4.22"
|
||||
nanoid = "0.4.0"
|
||||
openssl = "0.10.66"
|
||||
path-clean = "1.0.1"
|
||||
url = "2.5.2"
|
||||
51
README.md
Normal file
51
README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# gemini-grs
|
||||
|
||||
A Gemini protocol server written in Rust.
|
||||
|
||||
## TODO
|
||||
|
||||
- [x] Configuration via environment variables
|
||||
- [x] Proper logging
|
||||
- [x] Serve images
|
||||
- [ ] Read and send file in chunks instead of serving whole file from memory
|
||||
- [ ] Expand to serve other protocols (spartan, scroll, titan etc.)
|
||||
- [ ] Async I/O?
|
||||
|
||||
## External dependencies
|
||||
|
||||
OpenSSL
|
||||
|
||||
## Building
|
||||
|
||||
```shell
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
Generate your TLS keys:
|
||||
|
||||
```shell
|
||||
openssl genrsa -out key.pem 2048
|
||||
openssl req -new -key key.pem -out request.pem
|
||||
openssl x509 -req -days 36500 -in request.pem -signkey key.pem -out cert.pem
|
||||
```
|
||||
|
||||
Environment variables with examples:
|
||||
|
||||
- GEMINI_SERVER_HOSTNAME="0.0.0.0:1965"
|
||||
- GEMINI_SERVER_TLS_KEY_FILENAME="/where/you/put/key.pem"
|
||||
- GEMINI_SERVER_TLS_CERT_FILENAME="/where/you/put/cert.pem"
|
||||
- GEMINI_SERVER_ROOT_DIRECTORY="/files/to/serve"
|
||||
- RUST_LOG="debug"
|
||||
|
||||
Example command:
|
||||
|
||||
```shell
|
||||
GEMINI_SERVER_TLS_KEY_FILENAME=/home/user/server/key.pem \
|
||||
GEMINI_SERVER_TLS_CERT_FILENAME=/home/user/server/cert.pem \
|
||||
GEMINI_SERVER_ROOT_DIRECTORY=/gemini-root \
|
||||
RUST_LOG=debug \
|
||||
./target/release/gemini-grs
|
||||
```
|
||||
|
||||
9
src/fs.rs
Normal file
9
src/fs.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read};
|
||||
|
||||
pub fn read_file_as_bytes(path: &str) -> io::Result<Vec<u8>> {
|
||||
let mut file = File::open(path)?;
|
||||
let mut buffer = Vec::new();
|
||||
file.read_to_end(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
}
|
||||
63
src/gemini/mod.rs
Normal file
63
src/gemini/mod.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use std::fmt;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use url::Url;
|
||||
|
||||
pub mod server;
|
||||
pub mod tls;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GeminiUrl {
|
||||
pub scheme: String,
|
||||
pub hostname: String,
|
||||
pub port: u16,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for GeminiUrl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}://{}:{}{}",
|
||||
self.scheme, self.hostname, self.port, self.path
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl GeminiUrl {
|
||||
/// Default scheme: "gemini://"
|
||||
/// Port: 1965
|
||||
/// Path: "/"
|
||||
pub fn new(input: &str) -> Result<GeminiUrl> {
|
||||
let input_with_scheme = if input.contains("://") {
|
||||
input.to_string()
|
||||
} else {
|
||||
format!("gemini://{}", input)
|
||||
};
|
||||
let parsed = Url::parse(&input_with_scheme)
|
||||
.with_context(|| format!("Invalid request URL {}", &input_with_scheme))?;
|
||||
if parsed.scheme() != "gemini" {
|
||||
return Err(anyhow!(
|
||||
"Invalid request (URL protocol should be gemini://)"
|
||||
));
|
||||
}
|
||||
let hostname = parsed
|
||||
.host_str()
|
||||
.ok_or_else(|| anyhow!("Invalid request (URL must have a host)"))?;
|
||||
let mut parsed = GeminiUrl {
|
||||
scheme: parsed.scheme().to_string(),
|
||||
hostname: hostname.to_string(),
|
||||
port: parsed.port().unwrap_or(1965),
|
||||
path: parsed.path().to_string(),
|
||||
};
|
||||
if parsed.path.is_empty() {
|
||||
parsed.path = String::from("/")
|
||||
};
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
pub fn to_url(&self) -> Url {
|
||||
Url::parse(&self.to_string()).unwrap()
|
||||
}
|
||||
}
|
||||
233
src/gemini/server.rs
Normal file
233
src/gemini/server.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{IpAddr, TcpListener, TcpStream};
|
||||
use std::path::PathBuf;
|
||||
use std::thread;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use log;
|
||||
use log::{error, info};
|
||||
use openssl::ssl::{SslAcceptor, SslStream};
|
||||
use path_clean::PathClean;
|
||||
|
||||
use crate::fs::read_file_as_bytes;
|
||||
use crate::gemini::{GeminiUrl, tls};
|
||||
use crate::nanoid::nanoid;
|
||||
use crate::time::now_unix_millis;
|
||||
|
||||
struct GeminiSession {
|
||||
pub ip: IpAddr,
|
||||
pub tls_id: Option<String>,
|
||||
pub stream: SslStream<TcpStream>,
|
||||
pub id: String,
|
||||
pub timestamp: u128,
|
||||
}
|
||||
|
||||
struct GeminiResponse {
|
||||
code: u8,
|
||||
mime_type: Option<String>,
|
||||
data: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl GeminiResponse {
|
||||
pub fn new(
|
||||
srv_root: &str,
|
||||
gemini_session: &GeminiSession,
|
||||
path: &String,
|
||||
) -> Result<GeminiResponse> {
|
||||
let filename = resolve_path(srv_root, path)?;
|
||||
if !filename.is_file() {
|
||||
return Ok(GeminiResponse {
|
||||
code: 51, // NOT FOUND
|
||||
mime_type: None,
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
let mime_type = Self::guess_mime_type(PathBuf::from(&filename));
|
||||
if mime_type.is_none() {
|
||||
log_info(
|
||||
gemini_session,
|
||||
&format!("Cannot guess mime type {:?}", filename),
|
||||
);
|
||||
return Ok(GeminiResponse {
|
||||
code: 51,
|
||||
mime_type: None,
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
log_info(gemini_session, &format!("Opening file {:?}", filename));
|
||||
let file_str = filename
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Failed to convert path to string"))?;
|
||||
// let data = read_file_to_string(file_str)
|
||||
// .with_context(|| format!("Failed to read file {}", path))?;
|
||||
let data = read_file_as_bytes(file_str)
|
||||
.with_context(|| format!("Failed to read file {}", path))?;
|
||||
Ok(GeminiResponse {
|
||||
code: 20,
|
||||
mime_type,
|
||||
data: Some(data),
|
||||
})
|
||||
}
|
||||
|
||||
fn guess_mime_type(path: PathBuf) -> Option<String> {
|
||||
match path.extension() {
|
||||
Some(e) => match e.to_str().unwrap_or("") {
|
||||
"gmi" => Some(String::from("text/gemini")),
|
||||
"jpg" => Some(String::from("image/jpeg")),
|
||||
"jpeg" => Some(String::from("image/jpeg")),
|
||||
"png" => Some(String::from("image/png")),
|
||||
_ => None,
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_bytes(&self) -> Vec<u8> {
|
||||
let header = match &self.mime_type {
|
||||
Some(e) => format!("{}\t{}\r\n", self.code, e),
|
||||
None => format!("{}\r\n", self.code),
|
||||
};
|
||||
// Convert the header to bytes and prepare the response vector
|
||||
let mut response_bytes = header.into_bytes();
|
||||
|
||||
// Extend the response with the data if it exists
|
||||
if let Some(e) = &self.data {
|
||||
response_bytes.extend(e);
|
||||
}
|
||||
response_bytes
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_server(
|
||||
bind_address: &str,
|
||||
key_file: &str,
|
||||
cert_file: &str,
|
||||
srv_root: &str,
|
||||
) -> Result<()> {
|
||||
let acceptor = tls::create_tls_acceptor(key_file, cert_file).context("TLS error")?;
|
||||
let listener = TcpListener::bind(bind_address).context("TCP bind error")?;
|
||||
info!("Gemini server listening to {}", &bind_address);
|
||||
|
||||
for stream in listener.incoming() {
|
||||
// The stream will be dropped at the end of
|
||||
// each loop iteration, which will close
|
||||
// the stream automatically. No housekeeping
|
||||
// necessary.
|
||||
// Each stream is handled in a new thread.
|
||||
// Async is under consideration for now.
|
||||
match stream {
|
||||
Err(e) => {
|
||||
error!("Failed to accept connection: {:?}", e);
|
||||
}
|
||||
Ok(stream) => {
|
||||
let acceptor = acceptor.clone();
|
||||
let srv_root = srv_root.to_string();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
if let Err(e) = initiate_connection(&acceptor, stream, &srv_root) {
|
||||
error!("Connection handling error: {:?}", e);
|
||||
}
|
||||
});
|
||||
if let Err(e) = handle.join() {
|
||||
error!("Thread join error while handling stream: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn log_info(session: &GeminiSession, message: &str) {
|
||||
info!("{} {} {}", session.id, session.ip.to_string(), message);
|
||||
}
|
||||
|
||||
/// Resolve final absolute path
|
||||
/// of the file to serve.
|
||||
/// Final path must always begin with
|
||||
/// root path (no escape outside root!).
|
||||
/// If final path is a directory, append
|
||||
/// index.gmi.
|
||||
fn resolve_path(root_path: &str, input: &String) -> Result<PathBuf> {
|
||||
let path1 = PathBuf::from(root_path);
|
||||
let mut path2 = PathBuf::from(input);
|
||||
if path2.is_absolute() {
|
||||
path2 = PathBuf::from(path2.strip_prefix("/")?);
|
||||
}
|
||||
let mut final_path = path1.join(path2).clean();
|
||||
if !final_path.starts_with(path1) {
|
||||
return Err(anyhow!("Invalid path {:?} -> {:?}", input, final_path));
|
||||
}
|
||||
if final_path.is_dir() {
|
||||
final_path = final_path.join("index.gmi");
|
||||
}
|
||||
Ok(final_path)
|
||||
}
|
||||
|
||||
fn handle_gemini_session(srv_root: &str, gemini_session: &mut GeminiSession) -> Result<()> {
|
||||
let mut buffer = [0; 1024];
|
||||
let bytes_read = gemini_session
|
||||
.stream
|
||||
.read(&mut buffer)
|
||||
.context("Invalid request (failed to read input stream)")?;
|
||||
let received = String::from_utf8(Vec::from(&buffer[..bytes_read]))
|
||||
.context("Invalid request (failed to convert input to UTF8)")?;
|
||||
let request_data = received.trim();
|
||||
let url = GeminiUrl::new(request_data)?;
|
||||
log_info(
|
||||
gemini_session,
|
||||
&format!(
|
||||
"New request from {}{} {}",
|
||||
gemini_session.ip,
|
||||
match &gemini_session.tls_id {
|
||||
Some(x) => format!(" TLS digest {}", x),
|
||||
None => String::from(""),
|
||||
},
|
||||
request_data
|
||||
),
|
||||
);
|
||||
let response = GeminiResponse::new(srv_root, gemini_session, &url.path)?;
|
||||
let response_raw = response.get_bytes();
|
||||
log_info(
|
||||
gemini_session,
|
||||
&format!(
|
||||
"Reply Code {} Response length {} bytes",
|
||||
response.code,
|
||||
response_raw.len()
|
||||
),
|
||||
);
|
||||
gemini_session
|
||||
.stream
|
||||
.write_all(&response_raw)
|
||||
.context("Failed to write response to stream")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn initiate_connection(acceptor: &SslAcceptor, stream: TcpStream, srv_root: &str) -> Result<()> {
|
||||
let timestamp1 = now_unix_millis();
|
||||
|
||||
let ip = stream
|
||||
.peer_addr()
|
||||
.context("Failed to get peer IP address from TCP stream")?
|
||||
.ip();
|
||||
|
||||
let stream = acceptor
|
||||
.accept(stream)
|
||||
.with_context(|| format!("{} Failed to establish SSL connection", ip))?;
|
||||
|
||||
let mut gemini_session = GeminiSession {
|
||||
ip,
|
||||
tls_id: tls::get_peer_certificate_digest(&stream),
|
||||
stream,
|
||||
id: nanoid(),
|
||||
timestamp: now_unix_millis(),
|
||||
};
|
||||
|
||||
handle_gemini_session(srv_root, &mut gemini_session)?;
|
||||
|
||||
log_info(
|
||||
&gemini_session,
|
||||
&format!("Finished ({}ms)", now_unix_millis() - timestamp1),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
70
src/gemini/tls.rs
Normal file
70
src/gemini/tls.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use std::net::TcpStream;
|
||||
|
||||
use hex::ToHex;
|
||||
use openssl::hash::MessageDigest;
|
||||
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslStream, SslVerifyMode};
|
||||
|
||||
pub fn create_tls_acceptor(key_filename: &str, cert_filename: &str) -> anyhow::Result<SslAcceptor> {
|
||||
let mut acceptor = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;
|
||||
acceptor.set_private_key_file(key_filename, SslFiletype::PEM)?;
|
||||
acceptor.set_certificate_chain_file(cert_filename)?;
|
||||
|
||||
// Enable peer verification
|
||||
acceptor.set_verify(SslVerifyMode::PEER);
|
||||
|
||||
// Custom verification callback
|
||||
acceptor.set_verify_callback(SslVerifyMode::PEER, |preverify_ok, x509_ctx| {
|
||||
if preverify_ok {
|
||||
return true;
|
||||
}
|
||||
// Check if it's a self-signed certificate
|
||||
let cert = match x509_ctx.current_cert() {
|
||||
Some(cert) => cert,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let issuer_name = cert.issuer_name().to_der();
|
||||
let subject_name = cert.subject_name().to_der();
|
||||
|
||||
let is_self_signed = match (issuer_name, subject_name) {
|
||||
(Ok(issuer), Ok(subject)) => issuer == subject,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
if is_self_signed {
|
||||
let public_key = match cert.public_key() {
|
||||
Ok(key) => key,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
if cert.verify(&public_key).is_ok() {
|
||||
let now = match openssl::asn1::Asn1Time::days_from_now(0) {
|
||||
Ok(time) => time,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
if cert.not_before() <= now && cert.not_after() >= now {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
});
|
||||
// Initialize the session ID context
|
||||
let context = &crate::nanoid::nanoid().into_bytes();
|
||||
acceptor.set_session_id_context(context)?;
|
||||
acceptor.check_private_key()?;
|
||||
Ok(acceptor.build())
|
||||
}
|
||||
|
||||
pub fn get_peer_certificate_digest(stream: &SslStream<TcpStream>) -> Option<String> {
|
||||
if let Some(cert) = stream.ssl().peer_certificate() {
|
||||
if let Ok(digest) = cert.digest(MessageDigest::ripemd160()) {
|
||||
Some(digest.encode_hex::<String>())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
4
src/lib.rs
Normal file
4
src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod fs;
|
||||
pub mod gemini;
|
||||
pub mod nanoid;
|
||||
pub mod time;
|
||||
61
src/main.rs
Normal file
61
src/main.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use std::env;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use log::error;
|
||||
|
||||
use gemini_grs::gemini;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EnvConfig {
|
||||
pub hostname: String,
|
||||
pub key_path: String,
|
||||
pub cert_path: String,
|
||||
pub server_root: String,
|
||||
}
|
||||
|
||||
impl EnvConfig {
|
||||
pub fn from_env() -> Result<Self> {
|
||||
let hostname = env::var("GEMINI_SERVER_HOSTNAME").unwrap_or("127.0.0.1:1965".to_string());
|
||||
let key_path = env::var("GEMINI_SERVER_TLS_KEY_FILENAME").with_context(|| {
|
||||
"Missing environment variable GEMINI_SERVER_TLS_KEY_FILENAME".to_string()
|
||||
})?;
|
||||
let cert_path = env::var("GEMINI_SERVER_TLS_CERT_FILENAME").with_context(|| {
|
||||
"Missing environment variable GEMINI_SERVER_TLS_CERT_FILENAME".to_string()
|
||||
})?;
|
||||
let server_root = env::var("GEMINI_SERVER_ROOT_DIRECTORY").with_context(|| {
|
||||
"Missing environment variable GEMINI_SERVER_ROOT_DIRECTORY".to_string()
|
||||
})?;
|
||||
Ok(Self {
|
||||
hostname,
|
||||
key_path,
|
||||
cert_path,
|
||||
server_root,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let exit_code = run();
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
fn run() -> i32 {
|
||||
env_logger::init();
|
||||
let env_config = match EnvConfig::from_env() {
|
||||
Err(e) => {
|
||||
error!("{:#?}", e);
|
||||
return 1;
|
||||
}
|
||||
Ok(x) => x,
|
||||
};
|
||||
if let Err(e) = gemini::server::start_server(
|
||||
&env_config.hostname,
|
||||
&env_config.key_path,
|
||||
&env_config.cert_path,
|
||||
&env_config.server_root,
|
||||
) {
|
||||
error!("{:#?}", e);
|
||||
return 1;
|
||||
}
|
||||
0
|
||||
}
|
||||
11
src/nanoid.rs
Normal file
11
src/nanoid.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use nanoid::nanoid;
|
||||
|
||||
pub fn nanoid() -> String {
|
||||
let alphabet = [
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
|
||||
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T',
|
||||
'U','V','W','X','Y','Z',
|
||||
];
|
||||
nanoid!(16, &alphabet)
|
||||
}
|
||||
9
src/time.rs
Normal file
9
src/time.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
// returns the time since the epoch in milliseconds
|
||||
pub fn now_unix_millis() -> u128 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis()
|
||||
}
|
||||
Reference in New Issue
Block a user