Compare commits

...

37 Commits
0.2.3 ... 0.4.1

Author SHA1 Message Date
Greg Heartsfield
6ca3e3ffea build: bump version to 0.4.1 2022-01-26 21:48:44 -06:00
Greg Heartsfield
49c668a07c improvement: upgrade dependency (h2) 2022-01-26 21:48:11 -06:00
Greg Heartsfield
98c6fa6f39 feat: allow whitelisting of pubkeys for new events
This adds a configuration option, `authorization.pubkey_whitelist`
which is an array of pubkeys that are allowed to publish events on
this relay.
2022-01-26 21:39:03 -06:00
Greg Heartsfield
452bbbb0e5 docs: update feature list (NIP-12, prefix search) 2022-01-26 07:24:04 -06:00
Greg Heartsfield
ee0de6f875 improvement: clearer and less verbose database logging 2022-01-25 21:42:43 -06:00
Greg Heartsfield
699489ebaf build: bump version to 0.4.0 2022-01-25 20:56:00 -06:00
Greg Heartsfield
af9da65f71 improvement: upgrade dependencies 2022-01-25 20:55:29 -06:00
Greg Heartsfield
a72eaec3b8 fix: never display hidden events 2022-01-25 20:48:46 -06:00
Greg Heartsfield
f1206e76f2 feat: database reader connection pooling
Added connection pooling for queries, as well as basic configuration
options for min/max connections.
2022-01-25 20:39:24 -06:00
Greg Heartsfield
af453548ee feat: allow author and event id prefix search
This is an experimental non-NIP feature that allows a subscription
filter to include a prefix for authors and events.
2022-01-25 18:23:08 -06:00
Greg Heartsfield
df251c821c docs: updated discord invite link 2022-01-25 07:43:15 -06:00
Greg Heartsfield
2d28a95ff7 feat: allow arbitrary tag queries
This is an experimental feature, outside of any NIP, that demonstrates
generic tag queries.

Instead of limiting subscription filters to just querying only "e" or
"p" tags (via `#e` or `#p` attributes), any tag can be queried.

As an example, consider an event which uses a tag "url".  With this
modification, a subscription filter could add a top-level field
"#url", with an array of strings as the key.  Exact matches would be
returned.

A NIP is forthcoming to formalize this.
2022-01-22 21:29:15 -06:00
Greg Heartsfield
8c93ef5bc2 docs: provide public docker hub link 2022-01-20 22:02:42 -06:00
Greg Heartsfield
1c0fc1326d docs: add timeout for reverse-proxy example 2022-01-19 21:19:12 -06:00
Raj
179928378e refactor: add strictly typed tags
* Add custom error variant

This can be useful to propagate errors not conforming to available
variants. Also to convert other errors in `crate::Error` without having
explicit conversion defined, with `error.to_string()`

* Implement `Tag` and define protocol serialization

A Tag structure have been implemented with dedicated field types. Then
custom serde serialization is derived to map the structure to current
protocol json array as per NIP01.

This adds compile and run time type checking to always ensure wrong
string data are never stored or processed. With strict typed fields and
custom serde derivation this checks can be done at time of serialization,
saving work for internal handling of the actual data.

tests for possible data violations are added, and gives good example of
kind of errors it will through for different cases.

* Use String for URL
2022-01-19 07:42:58 -06:00
Raj
c605d75bb4 docs: update readme to include the new discord server 2022-01-17 08:35:13 -06:00
Greg Heartsfield
81e4e2b892 feat: add supported NIPs (2, 11) to relay info 2022-01-16 08:37:21 -06:00
Greg Heartsfield
6f166433b5 fix: test failures 2022-01-16 08:36:52 -06:00
Greg Heartsfield
030b64de62 feat: replace email with contact field in relay info.
This finalizes the NIP-11 spec implementation.

Fixes https://todo.sr.ht/~gheartsfield/nostr-rs-relay/21.
2022-01-16 08:34:19 -06:00
Greg Heartsfield
c7eadb1154 Add feature list to README 2022-01-16 08:16:42 -06:00
Greg Heartsfield
62dc77369d docs: rename example relay server 2022-01-15 11:43:12 -06:00
Greg Heartsfield
24587435ca docs: reverse proxy example 2022-01-15 11:41:31 -06:00
Greg Heartsfield
a3124ccea4 improvement: better sql error handling 2022-01-15 09:42:53 -06:00
Greg Heartsfield
4e51e61d16 improvement: display rate limit messages max once per sec 2022-01-15 09:42:17 -06:00
Raj
5c8390bbe0 fix: fix some test failures 2022-01-14 14:27:12 -06:00
Greg Heartsfield
da7968efef fix: restore working websocket message size configuration options 2022-01-05 17:41:12 -05:00
Greg Heartsfield
7037555516 improvement: add indexed tag queries 2022-01-05 17:33:53 -05:00
Greg Heartsfield
19ed990c57 refactor: fix clippy errors for relay info response 2022-01-05 10:10:44 -05:00
Greg Heartsfield
d78bbfc290 build: bump version to 0.3.3 2022-01-03 22:07:15 -05:00
Greg Heartsfield
2924da88bc feat: incorporated improvements from NIP-11 discussion
Change descr to description.  Add `id` for websocket URL.  Use
integers for supported NIPs instead of strings.  Top-level is object,
instead of the array before.
2022-01-03 22:03:30 -05:00
Greg Heartsfield
3024e9fba4 build: bump version to 0.3.2 2022-01-03 18:43:17 -05:00
Greg Heartsfield
d3da4eb009 feat: implementation of proposed NIP-11 (server metadata) 2022-01-03 18:42:24 -05:00
Greg Heartsfield
19637d612e build: bump version to 0.3.1 2022-01-01 19:26:15 -06:00
Greg Heartsfield
afc9a0096a improvement: logging failed queries and timing 2022-01-01 19:25:09 -06:00
Greg Heartsfield
3d56262386 build: bump version to 0.3.0 2022-01-01 18:40:57 -06:00
Greg Heartsfield
6673fcfd11 feat: implement multi-valued filter searching
NIP-01 now uses arrays instead of scalars.

Fixes https://todo.sr.ht/~gheartsfield/nostr-rs-relay/17
2022-01-01 18:38:52 -06:00
Greg Heartsfield
b5da3fa2b0 docs: link to docker hub 2022-01-01 12:27:09 -06:00
15 changed files with 1420 additions and 297 deletions

235
Cargo.lock generated
View File

@@ -63,7 +63,7 @@ version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce18265ec2324ad075345d5814fbeed4f41f0a660055dc78840b74d19b874b1"
dependencies = [
"serde 1.0.131",
"serde 1.0.136",
]
[[package]]
@@ -89,9 +89,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.8.0"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
[[package]]
name = "byteorder"
@@ -135,7 +135,7 @@ dependencies = [
"lazy_static",
"nom",
"rust-ini",
"serde 1.0.131",
"serde 1.0.136",
"serde-hjson",
"serde_json",
"toml",
@@ -153,9 +153,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120"
dependencies = [
"cfg-if",
"lazy_static",
@@ -163,13 +163,12 @@ dependencies = [
[[package]]
name = "dashmap"
version = "5.0.0"
version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b799062aaf67eb976af3bdca031ee6f846d2f0a5710ddbb0d2efee33f3cc4760"
checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c"
dependencies = [
"cfg-if",
"num_cpus",
"parking_lot",
]
[[package]]
@@ -230,9 +229,9 @@ checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]]
name = "futures"
version = "0.3.18"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e"
checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4"
dependencies = [
"futures-channel",
"futures-core",
@@ -245,9 +244,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.18"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27"
checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b"
dependencies = [
"futures-core",
"futures-sink",
@@ -255,15 +254,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.18"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7"
[[package]]
name = "futures-executor"
version = "0.3.18"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a"
dependencies = [
"futures-core",
"futures-task",
@@ -272,15 +271,15 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.18"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11"
checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2"
[[package]]
name = "futures-macro"
version = "0.3.18"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd"
checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c"
dependencies = [
"proc-macro2",
"quote",
@@ -289,15 +288,15 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.18"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af"
checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508"
[[package]]
name = "futures-task"
version = "0.3.18"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72"
[[package]]
name = "futures-timer"
@@ -307,9 +306,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
[[package]]
name = "futures-util"
version = "0.3.18"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164"
dependencies = [
"futures-channel",
"futures-core",
@@ -325,9 +324,9 @@ dependencies = [
[[package]]
name = "generic-array"
version = "0.14.4"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
dependencies = [
"typenum",
"version_check",
@@ -335,9 +334,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.3"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
dependencies = [
"cfg-if",
"libc",
@@ -346,9 +345,9 @@ dependencies = [
[[package]]
name = "governor"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3de427a64787873c3b196285e6684cddbf0ae7d1d8d56eaafbb4120c4cb641"
checksum = "7df0ee4b237afb71e99f7e2fbd840ffec2d6c4bb569f69b2af18aa1f63077d38"
dependencies = [
"dashmap",
"futures",
@@ -363,9 +362,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.9"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f072413d126e57991455e0a922b31e4c8ba7c2ffbebf6b78b4f8521397d65cd"
checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e"
dependencies = [
"bytes",
"fnv",
@@ -415,13 +414,13 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "http"
version = "0.2.5"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03"
dependencies = [
"bytes",
"fnv",
"itoa",
"itoa 1.0.1",
]
[[package]]
@@ -468,7 +467,7 @@ dependencies = [
"http-body",
"httparse",
"httpdate",
"itoa",
"itoa 0.4.8",
"pin-project-lite",
"socket2",
"tokio",
@@ -490,9 +489,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
dependencies = [
"autocfg 1.0.1",
"hashbrown",
@@ -514,10 +513,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "js-sys"
version = "0.3.55"
name = "itoa"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
[[package]]
name = "js-sys"
version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04"
dependencies = [
"wasm-bindgen",
]
@@ -543,9 +548,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.111"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e167738f1866a7ec625567bae89ca0d44477232a4f7c52b1c7f2adc2c98804f"
checksum = "b0005d08a8f7b65fb8073cb697aa0b12b631ed251ce73d862ce50eeb52ce3b50"
[[package]]
name = "libsqlite3-sys"
@@ -649,7 +654,7 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
[[package]]
name = "nostr-rs-relay"
version = "0.2.3"
version = "0.4.1"
dependencies = [
"bitcoin_hashes 0.9.7",
"config",
@@ -662,9 +667,11 @@ dependencies = [
"lazy_static",
"log",
"nonzero_ext",
"r2d2",
"r2d2_sqlite",
"rusqlite",
"secp256k1",
"serde 1.0.131",
"serde 1.0.136",
"serde_json",
"thiserror",
"tokio",
@@ -702,9 +709,9 @@ dependencies = [
[[package]]
name = "num_cpus"
version = "1.13.0"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
dependencies = [
"hermit-abi",
"libc",
@@ -712,9 +719,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.8.0"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
[[package]]
name = "opaque-debug"
@@ -755,9 +762,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pin-project-lite"
version = "0.2.7"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
[[package]]
name = "pin-utils"
@@ -773,15 +780,15 @@ checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
[[package]]
name = "ppv-lite86"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]]
name = "proc-macro2"
version = "1.0.33"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb37d2df5df740e582f28f8560cf425f52bb267d872fe58358eadb554909f07a"
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
dependencies = [
"unicode-xid",
]
@@ -804,13 +811,34 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.10"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r2d2"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f"
dependencies = [
"log",
"parking_lot",
"scheduled-thread-pool",
]
[[package]]
name = "r2d2_sqlite"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54ca3c9468a76fc2ad724c486a59682fc362efeac7b18d1c012958bc19f34800"
dependencies = [
"r2d2",
"rusqlite",
]
[[package]]
name = "rand"
version = "0.6.5"
@@ -1024,9 +1052,18 @@ checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2"
[[package]]
name = "ryu"
version = "1.0.7"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "254df5081ce98661a883445175e52efe99d1cb2a5552891d965d2f5d0cad1c16"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
[[package]]
name = "scheduled-thread-pool"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7"
dependencies = [
"parking_lot",
]
[[package]]
name = "scopeguard"
@@ -1042,7 +1079,7 @@ dependencies = [
"bitcoin_hashes 0.10.0",
"rand 0.6.5",
"secp256k1-sys",
"serde 1.0.131",
"serde 1.0.136",
]
[[package]]
@@ -1061,9 +1098,9 @@ checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8"
[[package]]
name = "serde"
version = "1.0.131"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1"
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
dependencies = [
"serde_derive",
]
@@ -1082,9 +1119,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.131"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b710a83c4e0dff6a3d511946b95274ad9ca9e5d3ae497b63fda866ac955358d2"
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
dependencies = [
"proc-macro2",
"quote",
@@ -1093,13 +1130,14 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.72"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527"
checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085"
dependencies = [
"itoa",
"indexmap",
"itoa 1.0.1",
"ryu",
"serde 1.0.131",
"serde 1.0.136",
]
[[package]]
@@ -1132,15 +1170,15 @@ checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
[[package]]
name = "smallvec"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
[[package]]
name = "socket2"
version = "0.4.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
checksum = "0f82496b90c36d70af5fcd482edaa2e0bd16fade569de1330405fecbbdac736b"
dependencies = [
"libc",
"winapi",
@@ -1154,9 +1192,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "1.0.82"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59"
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
dependencies = [
"proc-macro2",
"quote",
@@ -1209,11 +1247,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.14.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838"
dependencies = [
"autocfg 1.0.1",
"bytes",
"libc",
"memchr",
@@ -1229,9 +1266,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "1.6.0"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e"
checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
dependencies = [
"proc-macro2",
"quote",
@@ -1270,7 +1307,7 @@ version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
dependencies = [
"serde 1.0.131",
"serde 1.0.136",
]
[[package]]
@@ -1326,9 +1363,9 @@ dependencies = [
[[package]]
name = "typenum"
version = "1.14.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unicode-bidi"
@@ -1386,9 +1423,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.3"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "want"
@@ -1408,9 +1445,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "wasm-bindgen"
version = "0.2.78"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@@ -1418,9 +1455,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.78"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca"
dependencies = [
"bumpalo",
"lazy_static",
@@ -1433,9 +1470,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.78"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1443,9 +1480,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.78"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc"
dependencies = [
"proc-macro2",
"quote",
@@ -1456,15 +1493,15 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.78"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2"
[[package]]
name = "web-sys"
version = "0.3.55"
version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb"
dependencies = [
"js-sys",
"wasm-bindgen",

View File

@@ -1,6 +1,6 @@
[package]
name = "nostr-rs-relay"
version = "0.2.3"
version = "0.4.1"
edition = "2021"
[dependencies]
@@ -17,9 +17,11 @@ config = { version = "0.11", features = ["toml"] }
bitcoin_hashes = { version = "^0.9", features = ["serde"] }
secp256k1 = {git = "https://github.com/rust-bitcoin/rust-secp256k1.git", rev = "50034ccb18fdd84904ab3aa6c84a12fcced33209", features = ["rand", "rand-std", "serde", "bitcoin_hashes"] }
serde = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0"
serde_json = {version = "^1.0", features = ["preserve_order"]}
hex = "^0.4"
rusqlite = "^0.26"
rusqlite = { version = "^0.26", features = ["limits"]}
r2d2 = "^0.8"
r2d2_sqlite = "^0.19"
lazy_static = "^1.4"
governor = "^0.4"
nonzero_ext = "^0.3"

View File

@@ -8,6 +8,20 @@ The project master repository is available on
[sourcehut](https://sr.ht/~gheartsfield/nostr-rs-relay/), and is
mirrored on [GitHub](https://github.com/scsibug/nostr-rs-relay).
## Features
NIPs with a relay-specific implementation are listed here.
- [x] NIP-01: Core event model
- [x] NIP-01: Hide old metadata events
- [x] NIP-01: Id/Author prefix search (_experimental_)
- [x] NIP-02: Hide old contact list events
- [ ] NIP-03: OpenTimestamps
- [ ] NIP-05: Mapping Nostr keys to DNS identifiers
- [ ] NIP-09: Event deletion
- [x] NIP-11: Relay information document
- [x] NIP-12: Generic tag search (_experimental_)
## Quick Start
The provided `Dockerfile` will compile and build the server
@@ -39,9 +53,12 @@ Text Note [81cf...2652] from 296a...9b92 5 seconds ago
hello world
```
A pre-built container is also available on DockerHub:
https://hub.docker.com/r/scsibug/nostr-rs-relay
## Configuration
The sample `[config.toml](config.toml)` file demonstrates the
The sample [`config.toml`](config.toml) file demonstrates the
configuration available to the relay. This file is optional, but may
be mounted into a docker container like so:
@@ -55,6 +72,16 @@ $ docker run -it -p 7000:8080 \
Options include rate-limiting, event size limits, and network address
settings.
## Reverse Proxy Configuration
For examples of putting the relay behind a reverse proxy (for TLS
termination, load balancing, and other features), see [Reverse
Proxy](reverse-proxy.md).
## Dev Channel
The current dev discussions for this project is happening at https://discord.gg/ufG6fH52Vk.
Drop in to query any development related questions.
License
---
This project is MIT licensed.

View File

@@ -1,13 +1,39 @@
# Nostr-rs-relay configuration
[info]
# The advertised URL for the Nostr websocket.
relay_url = "wss://nostr.example.com/"
# Relay information for clients. Put your unique server name here.
name = "nostr-rs-relay"
# Description
description = "A newly created nostr-rs-relay.\n\nCustomize this with your own info."
# Administrative contact pubkey
#pubkey = "0c2d168a4ae8ca58c9f1ab237b5df682599c6c7ab74307ea8b05684b60405d41"
# Administrative contact URI
#contact = "mailto:contact@example.com"
[database]
# Directory for SQLite files. Defaults to the current directory. Can
# also be specified (and overriden) with the "--db dirname" command
# line option.
data_directory = "."
# Database connection pool settings:
# Minimum number of SQLite reader connections
#min_conn = 4
# Maximum number of SQLite reader connections
#max_conn = 128
[network]
# Bind to this network address
address = "0.0.0.0"
# Listen on this port
port = 8080
@@ -20,22 +46,30 @@ reject_future_seconds = 1800
[limits]
# Limit events created per second, averaged over one minute. Must be
# an integer. If not set (or set to 0), defaults to unlimited.
messages_per_sec = 0
#messages_per_sec = 0
# Limit the maximum size of an EVENT message. Defaults to 128 KB.
# Set to 0 for unlimited.
max_event_bytes = 131072
#max_event_bytes = 131072
# Maximum WebSocket message in bytes. Defaults to 128 KB.
max_ws_message_bytes = 131072
#max_ws_message_bytes = 131072
# Maximum WebSocket frame size in bytes. Defaults to 128 KB.
max_ws_frame_bytes = 131072
#max_ws_frame_bytes = 131072
# Broadcast buffer size, in number of events. This prevents slow
# readers from consuming memory. Defaults to 4096.
broadcast_buffer = 4096
#broadcast_buffer = 4096
# Event persistence buffer size, in number of events. This provides
# backpressure to senders if writes are slow. Defaults to 16.
event_persist_buffer = 16
#event_persist_buffer = 16
[authorization]
# Pubkey addresses in this array are whitelisted for event publishing.
# Only valid events by these authors will be accepted.
#pubkey_whitelist = [
# "35d26e4690cbe1a898af61cc3515661eb5fa763b57bd0b42e45099c8b32fd50f",
# "887645fef0ce0c3c1218d2f5d8e6132a19304cdc57cd20281d082f38cfea0072",
#]

53
reverse-proxy.md Normal file
View File

@@ -0,0 +1,53 @@
# Reverse Proxy Setup Guide
It is recommended to run `nostr-rs-relay` behind a reverse proxy such
as `haproxy` or `nginx` to provide TLS termination. A simple example
of an `haproxy` configuration is documented here.
## Minimal HAProxy Configuration
Assumptions:
* HAProxy version is `2.4.10` or greater (older versions not tested).
* Hostname for the relay is `relay.example.com`.
* Your relay should be available over wss://relay.example.com
* Your (NIP-11) relay info page should be available on https://relay.example.com
* SSL certificate is located in `/etc/certs/example.com.pem`.
* Relay is running on port 8080.
* Limit connections to 400 concurrent.
* HSTS (HTTP Strict Transport Security) is desired.
* Only TLS 1.2 or greater is allowed.
```
global
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
frontend fe_prod
mode http
bind :443 ssl crt /etc/certs/example.com.pem alpn h2,http/1.1
bind :80
http-request set-header X-Forwarded-Proto https if { ssl_fc }
redirect scheme https code 301 if !{ ssl_fc }
acl host_relay hdr(host) -i relay.example.com
use_backend relay if host_relay
# HSTS (1 year)
http-response set-header Strict-Transport-Security max-age=31536000
backend relay
mode http
timeout connect 5s
timeout client 50s
timeout server 50s
timeout tunnel 1h
timeout client-fin 30s
option tcp-check
default-server maxconn 400 check inter 20s fastinter 1s
server relay 127.0.0.1:8080
```
### Notes
You may experience WebSocket connection problems with Firefox if
HTTP/2 is enabled, for older versions of HAProxy (2.3.x). Either
disable HTTP/2 (`h2`), or upgrade HAProxy.

View File

@@ -8,10 +8,22 @@ lazy_static! {
pub static ref SETTINGS: RwLock<Settings> = RwLock::new(Settings::default());
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[allow(unused)]
pub struct Info {
pub relay_url: Option<String>,
pub name: Option<String>,
pub description: Option<String>,
pub pubkey: Option<String>,
pub contact: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(unused)]
pub struct Database {
pub data_directory: String,
pub min_conn: u32,
pub max_conn: u32,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -49,12 +61,20 @@ pub struct Limits {
pub event_persist_buffer: usize, // events to buffer for database commits (block senders if database writes are too slow)
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(unused)]
pub struct Authorization {
pub pubkey_whitelist: Option<Vec<String>>, // If present, only allow these pubkeys to publish events
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(unused)]
pub struct Settings {
pub info: Info,
pub database: Database,
pub network: Network,
pub limits: Limits,
pub authorization: Authorization,
pub retention: Retention,
pub options: Options,
}
@@ -82,6 +102,13 @@ impl Settings {
// override with file contents
.with_merged(config::File::with_name("config"))?
.try_into()?;
// ensure connection pool size is logical
if settings.database.min_conn > settings.database.max_conn {
panic!(
"Database min_conn setting ({}) cannot exceed max_conn ({})",
settings.database.min_conn, settings.database.max_conn
);
}
Ok(settings)
}
}
@@ -89,8 +116,17 @@ impl Settings {
impl Default for Settings {
fn default() -> Self {
Settings {
info: Info {
relay_url: None,
name: Some("Unnamed nostr-rs-relay".to_owned()),
description: None,
pubkey: None,
contact: None,
},
database: Database {
data_directory: ".".to_owned(),
min_conn: 4,
max_conn: 128,
},
network: Network {
port: 8080,
@@ -104,6 +140,9 @@ impl Default for Settings {
broadcast_buffer: 4096,
event_persist_buffer: 16,
},
authorization: Authorization {
pubkey_whitelist: None, // Allow any address to publish
},
retention: Retention {
max_events: None, // max events
max_bytes: None, // max size

550
src/db.rs
View File

@@ -1,4 +1,5 @@
//! Event persistence and querying
use crate::config;
use crate::error::Result;
use crate::event::Event;
use crate::subscription::Subscription;
@@ -11,10 +12,18 @@ use rusqlite::Connection;
use rusqlite::OpenFlags;
//use std::num::NonZeroU32;
use crate::config::SETTINGS;
use r2d2;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::limits::Limit;
use rusqlite::types::ToSql;
use std::path::Path;
use std::thread;
use std::time::Duration;
use std::time::Instant;
use tokio::task;
pub type SqlitePool = r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>;
/// Database file
const DB_FILE: &str = "nostr.db";
@@ -33,7 +42,7 @@ PRAGMA journal_mode=WAL;
PRAGMA main.synchronous=NORMAL;
PRAGMA foreign_keys = ON;
PRAGMA application_id = 1654008667;
PRAGMA user_version = 2;
PRAGMA user_version = 3;
-- Event Table
CREATE TABLE IF NOT EXISTS event (
@@ -53,6 +62,21 @@ CREATE INDEX IF NOT EXISTS created_at_index ON event(created_at);
CREATE INDEX IF NOT EXISTS author_index ON event(author);
CREATE INDEX IF NOT EXISTS kind_index ON event(kind);
-- Tag Table
-- Tag values are stored as either a BLOB (if they come in as a
-- hex-string), or TEXT otherwise.
-- This means that searches need to select the appropriate column.
CREATE TABLE IF NOT EXISTS tag (
id INTEGER PRIMARY KEY,
event_id INTEGER NOT NULL, -- an event ID that contains a tag.
name TEXT, -- the tag name ("p", "e", whatever)
value TEXT, -- the tag value, if not hex.
value_hex BLOB, -- the tag value, if it can be interpreted as a hex string.
FOREIGN KEY(event_id) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS tag_val_index ON tag(value);
CREATE INDEX IF NOT EXISTS tag_val_hex_index ON tag(value_hex);
-- Event References Table
CREATE TABLE IF NOT EXISTS event_ref (
id INTEGER PRIMARY KEY,
@@ -76,22 +100,66 @@ FOREIGN KEY(event_id) REFERENCES event(id) ON UPDATE RESTRICT ON DELETE CASCADE
CREATE INDEX IF NOT EXISTS pubkey_ref_index ON pubkey_ref(referenced_pubkey);
"##;
pub fn build_read_pool() -> SqlitePool {
let config = config::SETTINGS.read().unwrap();
let db_dir = &config.database.data_directory;
let full_path = Path::new(db_dir).join(DB_FILE);
// small hack; if the database doesn't exist yet, that means the
// writer thread hasn't finished. Give it a chance to work. This
// is only an issue with the first time we run.
while !full_path.exists() {
debug!("Database reader pool is waiting on the database to be created...");
thread::sleep(Duration::from_millis(500));
}
let manager = SqliteConnectionManager::file(&full_path)
.with_flags(OpenFlags::SQLITE_OPEN_READ_ONLY)
.with_init(|c| c.execute_batch(STARTUP_SQL));
let pool: SqlitePool = r2d2::Pool::builder()
.test_on_check_out(true) // no noticeable performance hit
.min_idle(Some(config.database.min_conn))
.max_size(config.database.max_conn)
.build(manager)
.unwrap();
info!(
"Built a connection pool (min={}, max={})",
config.database.min_conn, config.database.max_conn
);
return pool;
}
/// Upgrade DB to latest version, and execute pragma settings
pub fn upgrade_db(conn: &mut Connection) -> Result<()> {
// check the version.
let curr_version = db_version(conn)?;
let mut curr_version = db_version(conn)?;
info!("DB version = {:?}", curr_version);
debug!(
"SQLite max query parameters: {}",
conn.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER)
);
debug!(
"SQLite max table/blob/text length: {} MB",
(conn.limit(Limit::SQLITE_LIMIT_LENGTH) as f64 / (1024 * 1024) as f64).floor()
);
debug!(
"SQLite max SQL length: {} MB",
(conn.limit(Limit::SQLITE_LIMIT_SQL_LENGTH) as f64 / (1024 * 1024) as f64).floor()
);
// initialize from scratch
if curr_version == 0 {
match conn.execute_batch(INIT_SQL) {
Ok(()) => info!("database pragma/schema initialized to v2, and ready"),
Ok(()) => {
info!("database pragma/schema initialized to v3, and ready");
//curr_version = 3;
}
Err(err) => {
error!("update failed: {}", err);
panic!("database could not be initialized");
}
}
} else if curr_version == 1 {
}
if curr_version == 1 {
// only change is adding a hidden column to events.
let upgrade_sql = r##"
ALTER TABLE event ADD hidden INTEGER;
@@ -99,19 +167,73 @@ UPDATE event SET hidden=FALSE;
PRAGMA user_version = 2;
"##;
match conn.execute_batch(upgrade_sql) {
Ok(()) => info!("database schema upgraded v1 -> v2"),
Ok(()) => {
info!("database schema upgraded v1 -> v2");
curr_version = 2;
}
Err(err) => {
error!("update failed: {}", err);
panic!("database could not be upgraded");
}
}
} else if curr_version == 2 {
}
if curr_version == 2 {
// this version lacks the tag column
debug!("database schema needs update from 2->3");
let upgrade_sql = r##"
CREATE TABLE IF NOT EXISTS tag (
id INTEGER PRIMARY KEY,
event_id INTEGER NOT NULL, -- an event ID that contains a tag.
name TEXT, -- the tag name ("p", "e", whatever)
value TEXT, -- the tag value, if not hex.
value_hex BLOB, -- the tag value, if it can be interpreted as a hex string.
FOREIGN KEY(event_id) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS tag_val_index ON tag(value);
CREATE INDEX IF NOT EXISTS tag_val_hex_index ON tag(value_hex);
PRAGMA user_version = 3;
"##;
// TODO: load existing refs into tag table
match conn.execute_batch(upgrade_sql) {
Ok(()) => {
info!("database schema upgraded v2 -> v3");
//curr_version = 3;
}
Err(err) => {
error!("update failed: {}", err);
panic!("database could not be upgraded");
}
}
info!("Starting transaction");
// iterate over every event/pubkey tag
let tx = conn.transaction()?;
{
let mut stmt = tx.prepare("select event_id, \"e\", lower(hex(referenced_event)) from event_ref union select event_id, \"p\", lower(hex(referenced_pubkey)) from pubkey_ref;")?;
let mut tag_rows = stmt.query([])?;
while let Some(row) = tag_rows.next()? {
// we want to capture the event_id that had the tag, the tag name, and the tag hex value.
let event_id: u64 = row.get(0)?;
let tag_name: String = row.get(1)?;
let tag_value: String = row.get(2)?;
// this will leave behind p/e tags that were non-hex, but they are invalid anyways.
if is_hex(&tag_value) {
tx.execute(
"INSERT INTO tag (event_id, name, value_hex) VALUES (?1, ?2, ?3);",
params![event_id, tag_name, hex::decode(&tag_value).ok()],
)?;
}
}
}
tx.commit()?;
info!("Upgrade complete");
} else if curr_version == 3 {
debug!("Database version was already current");
} else if curr_version > 2 {
} else if curr_version > 3 {
panic!("Database version is newer than supported by this executable");
}
// Setup PRAGMA
conn.execute_batch(STARTUP_SQL)?;
info!("Finished pragma");
Ok(())
}
@@ -133,8 +255,13 @@ pub async fn db_writer(
)?;
info!("opened database {:?} for writing", full_path);
upgrade_db(&mut conn)?;
// Make a copy of the whitelist
let whitelist = &config.authorization.pubkey_whitelist.clone();
// get rate limit settings
let rps_setting = config.limits.messages_per_sec;
let mut most_recent_rate_limit = Instant::now();
let mut lim_opt = None;
let clock = governor::clock::QuantaClock::default();
if let Some(rps) = rps_setting {
@@ -157,12 +284,32 @@ pub async fn db_writer(
}
let mut event_write = false;
let event = next_event.unwrap();
// check if this event is authorized.
if let Some(allowed_addrs) = whitelist {
debug!("Checking against whitelist");
// if the event address is not in allowed_addrs.
if !allowed_addrs.contains(&event.pubkey) {
info!(
"Rejecting event {}, unauthorized author",
event.get_event_id_prefix()
);
// TODO: define a channel that can send NOTICEs back to the client.
continue;
}
}
let start = Instant::now();
match write_event(&mut conn, &event) {
Ok(updated) => {
if updated == 0 {
debug!("ignoring duplicate event");
} else {
info!("persisted event: {}", event.get_event_id_prefix());
info!(
"persisted event: {} in {:?}",
event.get_event_id_prefix(),
start.elapsed()
);
event_write = true;
// send this out to all clients
bcast_tx.send(event.clone()).ok();
@@ -176,8 +323,20 @@ pub async fn db_writer(
if event_write {
if let Some(ref lim) = lim_opt {
if let Err(n) = lim.check() {
info!("Rate limiting event creation");
thread::sleep(n.wait_time_from(clock.now()));
let wait_for = n.wait_time_from(clock.now());
// check if we have recently logged rate
// limits, but print out a message only once
// per second.
if most_recent_rate_limit.elapsed().as_secs() > 1 {
warn!(
"rate limit reached for event creation (sleep for {:?})",
wait_for
);
// reset last rate limit message
most_recent_rate_limit = Instant::now();
}
// block event writes, allowing them to queue up
thread::sleep(wait_for);
continue;
}
}
@@ -215,24 +374,24 @@ pub fn write_event(conn: &mut Connection, e: &Event) -> Result<usize> {
}
// remember primary key of the event most recently inserted.
let ev_id = tx.last_insert_rowid();
// add all event tags into the event_ref table
let etags = e.get_event_tags();
if !etags.is_empty() {
for etag in etags.iter() {
tx.execute(
"INSERT OR IGNORE INTO event_ref (event_id, referenced_event) VALUES (?1, ?2)",
params![ev_id, hex::decode(&etag).ok()],
)?;
}
}
// add all event tags into the pubkey_ref table
let ptags = e.get_pubkey_tags();
if !ptags.is_empty() {
for ptag in ptags.iter() {
tx.execute(
"INSERT OR IGNORE INTO pubkey_ref (event_id, referenced_pubkey) VALUES (?1, ?2)",
params![ev_id, hex::decode(&ptag).ok()],
)?;
// add all tags to the tag table
for tag in e.tags.iter() {
// ensure we have 2 values.
if tag.len() >= 2 {
let tagname = &tag[0];
let tagval = &tag[1];
// if tagvalue is hex;
if is_hex(tagval) {
tx.execute(
"INSERT OR IGNORE INTO tag (event_id, name, value_hex) VALUES (?1, ?2, ?3)",
params![ev_id, &tagname, hex::decode(&tagval).ok()],
)?;
} else {
tx.execute(
"INSERT OR IGNORE INTO tag (event_id, name, value) VALUES (?1, ?2, ?3)",
params![ev_id, &tagname, &tagval],
)?;
}
}
}
// if this event is for a metadata update, hide every other kind=0
@@ -275,60 +434,193 @@ fn is_hex(s: &str) -> bool {
s.chars().all(|x| char::is_ascii_hexdigit(&x))
}
/// Create a dynamic SQL query string from a subscription.
fn query_from_sub(sub: &Subscription) -> String {
/// Check if a string contains only f chars
fn is_all_fs(s: &str) -> bool {
s.chars().all(|x| x == 'f' || x == 'F')
}
#[derive(PartialEq, Debug, Clone)]
enum HexSearch {
// when no range is needed, exact 32-byte
Exact(Vec<u8>),
// lower (inclusive) and upper range (exclusive)
Range(Vec<u8>, Vec<u8>),
// lower bound only, upper bound is MAX inclusive
LowerOnly(Vec<u8>),
}
/// Find the next hex sequence greater than the argument.
fn hex_range(s: &str) -> Option<HexSearch> {
// handle special cases
if !is_hex(s) || s.len() > 64 {
return None;
}
if s.len() == 64 {
return Some(HexSearch::Exact(hex::decode(s).ok()?));
}
// if s is odd, add a zero
let mut hash_base = s.to_owned();
let mut odd = hash_base.len() % 2 != 0;
if odd {
// extend the string to make it even
hash_base.push('0');
}
let base = hex::decode(hash_base).ok()?;
// check for all ff's
if is_all_fs(s) {
// there is no higher bound, we only want to search for blobs greater than this.
return Some(HexSearch::LowerOnly(base));
}
// return a range
let mut upper = base.clone();
let mut byte_len = upper.len();
// for odd strings, we made them longer, but we want to increment the upper char (+16).
// we know we can do this without overflowing because we explicitly set the bottom half to 0's.
while byte_len > 0 {
byte_len -= 1;
// check if byte can be incremented, or if we need to carry.
let b = upper[byte_len];
if b == u8::MAX {
// reset and carry
upper[byte_len] = 0;
} else if odd {
// check if first char in this byte is NOT 'f'
if b < 240 {
upper[byte_len] = b + 16; // bump up the first character in this byte
// increment done, stop iterating through the vec
break;
} else {
// if it is 'f', reset the byte to 0 and do a carry
// reset and carry
upper[byte_len] = 0;
}
// done with odd logic, so don't repeat this
odd = false;
} else {
// bump up the first character in this byte
upper[byte_len] = b + 1;
// increment done, stop iterating
break;
}
}
Some(HexSearch::Range(base, upper))
}
fn repeat_vars(count: usize) -> String {
if count == 0 {
return "".to_owned();
}
let mut s = "?,".repeat(count);
// Remove trailing comma
s.pop();
s
}
/// Create a dynamic SQL query string and params from a subscription.
fn query_from_sub(sub: &Subscription) -> (String, Vec<Box<dyn ToSql>>) {
// build a dynamic SQL query. all user-input is either an integer
// (sqli-safe), or a string that is filtered to only contain
// hexadecimal characters.
// hexadecimal characters. Strings that require escaping (tag
// names/values) use parameters.
let mut query =
"SELECT DISTINCT(e.content) FROM event e LEFT JOIN event_ref er ON e.id=er.event_id LEFT JOIN pubkey_ref pr ON e.id=pr.event_id "
.to_owned();
"SELECT DISTINCT(e.content) FROM event e LEFT JOIN tag t ON e.id=t.event_id ".to_owned();
// parameters
let mut params: Vec<Box<dyn ToSql>> = vec![];
// for every filter in the subscription, generate a where clause
let mut filter_clauses: Vec<String> = Vec::new();
for f in sub.filters.iter() {
// individual filter components
let mut filter_components: Vec<String> = Vec::new();
// Query for "authors"
if f.authors.is_some() {
let authors_escaped: Vec<String> = f
.authors
.as_ref()
.unwrap()
.iter()
.filter(|&x| is_hex(x))
.map(|x| format!("x'{}'", x))
.collect();
let authors_clause = format!("author IN ({})", authors_escaped.join(", "));
// Query for "authors", allowing prefix matches
if let Some(authvec) = &f.authors {
// take each author and convert to a hexsearch
let mut auth_searches: Vec<String> = vec![];
for auth in authvec {
match hex_range(auth) {
Some(HexSearch::Exact(ex)) => {
auth_searches.push("author=?".to_owned());
params.push(Box::new(ex));
}
Some(HexSearch::Range(lower, upper)) => {
auth_searches.push("(author>? AND author<?)".to_owned());
params.push(Box::new(lower));
params.push(Box::new(upper));
}
Some(HexSearch::LowerOnly(lower)) => {
// info!("{:?} => lower; {:?} ", auth, hex::encode(lower));
auth_searches.push("author>?".to_owned());
params.push(Box::new(lower));
}
None => {
info!("Could not parse hex range from author {:?}", auth);
}
}
}
let authors_clause = format!("({})", auth_searches.join(" OR "));
filter_components.push(authors_clause);
}
// Query for Kind
if f.kind.is_some() {
if let Some(ks) = &f.kinds {
// kind is number, no escaping needed
let kind_clause = format!("kind = {}", f.kind.unwrap());
let str_kinds: Vec<String> = ks.iter().map(|x| x.to_string()).collect();
let kind_clause = format!("kind IN ({})", str_kinds.join(", "));
filter_components.push(kind_clause);
}
// Query for event
if f.id.is_some() {
let id_str = f.id.as_ref().unwrap();
if is_hex(id_str) {
let id_clause = format!("event_hash = x'{}'", id_str);
filter_components.push(id_clause);
// Query for event, allowing prefix matches
if let Some(idvec) = &f.ids {
// take each author and convert to a hexsearch
let mut id_searches: Vec<String> = vec![];
for id in idvec {
match hex_range(id) {
Some(HexSearch::Exact(ex)) => {
id_searches.push("event_hash=?".to_owned());
params.push(Box::new(ex));
}
Some(HexSearch::Range(lower, upper)) => {
id_searches.push("(event_hash>? AND event_hash<?)".to_owned());
params.push(Box::new(lower));
params.push(Box::new(upper));
}
Some(HexSearch::LowerOnly(lower)) => {
id_searches.push("event_hash>?".to_owned());
params.push(Box::new(lower));
}
None => {
info!("Could not parse hex range from id {:?}", id);
}
}
}
let id_clause = format!("({})", id_searches.join(" OR "));
filter_components.push(id_clause);
}
// Query for referenced event
if f.event.is_some() {
let ev_str = f.event.as_ref().unwrap();
if is_hex(ev_str) {
let ev_clause = format!("referenced_event = x'{}'", ev_str);
filter_components.push(ev_clause);
}
}
// Query for referenced pet name pubkey
if f.pubkey.is_some() {
let pet_str = f.pubkey.as_ref().unwrap();
if is_hex(pet_str) {
let pet_clause = format!("referenced_pubkey = x'{}'", pet_str);
filter_components.push(pet_clause);
// Query for tags
if let Some(map) = &f.tags {
for (key, val) in map.iter() {
let mut str_vals: Vec<Box<dyn ToSql>> = vec![];
let mut blob_vals: Vec<Box<dyn ToSql>> = vec![];
for v in val {
if is_hex(v) {
if let Ok(h) = hex::decode(&v) {
blob_vals.push(Box::new(h));
}
} else {
str_vals.push(Box::new(v.to_owned()));
}
}
// create clauses with "?" params for each tag value being searched
let str_clause = format!("value IN ({})", repeat_vars(str_vals.len()));
let blob_clause = format!("value_hex IN ({})", repeat_vars(blob_vals.len()));
let tag_clause = format!("(name=? AND ({} OR {}))", str_clause, blob_clause);
// add the tag name as the first parameter
params.push(Box::new(key.to_owned()));
// add all tag values that are plain strings as params
params.append(&mut str_vals);
// add all tag values that are blobs as params
params.append(&mut blob_vals);
filter_components.push(tag_clause);
}
}
// Query for timestamp
@@ -348,21 +640,22 @@ fn query_from_sub(sub: &Subscription) -> String {
fc.push_str(&filter_components.join(" AND "));
fc.push_str(" )");
filter_clauses.push(fc);
} else {
// never display hidden events
filter_clauses.push("hidden!=TRUE".to_owned());
}
}
// never display hidden events
query.push_str(" WHERE hidden!=TRUE ");
// combine all filters with OR clauses, if any exist
if !filter_clauses.is_empty() {
query.push_str(" WHERE ");
query.push_str(" AND (");
query.push_str(&filter_clauses.join(" OR "));
query.push_str(") ");
}
// add order clause
query.push_str(" ORDER BY created_at ASC");
debug!("query string: {}", query);
query
(query, params)
}
/// Perform a database query using a subscription.
@@ -373,31 +666,27 @@ fn query_from_sub(sub: &Subscription) -> String {
/// query is immediately aborted.
pub async fn db_query(
sub: Subscription,
conn: r2d2::PooledConnection<r2d2_sqlite::SqliteConnectionManager>,
query_tx: tokio::sync::mpsc::Sender<QueryResult>,
mut abandon_query_rx: tokio::sync::oneshot::Receiver<()>,
) {
task::spawn_blocking(move || {
let config = SETTINGS.read().unwrap();
let db_dir = &config.database.data_directory;
let full_path = Path::new(db_dir).join(DB_FILE);
let conn =
Connection::open_with_flags(&full_path, OpenFlags::SQLITE_OPEN_READ_ONLY).unwrap();
debug!("opened database for reading");
debug!("going to query for: {:?}", sub);
let mut row_count: usize = 0;
let start = Instant::now();
// generate SQL query
let q = query_from_sub(&sub);
// execute the query
let mut stmt = conn.prepare(&q).unwrap();
let mut event_rows = stmt.query([]).unwrap();
while let Some(row) = event_rows.next().unwrap() {
let (q, p) = query_from_sub(&sub);
// execute the query. Don't cache, since queries vary so much.
let mut stmt = conn.prepare(&q)?;
let mut event_rows = stmt.query(rusqlite::params_from_iter(p))?;
while let Some(row) = event_rows.next()? {
// check if this is still active (we could do this every N rows)
if abandon_query_rx.try_recv().is_ok() {
debug!("query aborted");
return;
return Ok(());
}
// TODO: check before unwrapping
let event_json = row.get(0).unwrap();
row_count += 1;
let event_json = row.get(0)?;
query_tx
.blocking_send(QueryResult {
sub_id: sub.get_id(),
@@ -405,6 +694,93 @@ pub async fn db_query(
})
.ok();
}
debug!("query completed");
debug!(
"query completed ({} rows) in {:?}",
row_count,
start.elapsed()
);
let ok: Result<()> = Ok(());
ok
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hex_range_exact() -> Result<()> {
let hex = "abcdef00abcdef00abcdef00abcdef00abcdef00abcdef00abcdef00abcdef00";
let r = hex_range(hex);
assert_eq!(
r,
Some(HexSearch::Exact(hex::decode(hex).expect("invalid hex")))
);
Ok(())
}
#[test]
fn hex_full_range() -> Result<()> {
//let hex = "abcdef00abcdef00abcdef00abcdef00abcdef00abcdef00abcdef00abcdef00";
let hex = "aaaa";
let hex_upper = "aaab";
let r = hex_range(hex);
assert_eq!(
r,
Some(HexSearch::Range(
hex::decode(hex).expect("invalid hex"),
hex::decode(hex_upper).expect("invalid hex")
))
);
Ok(())
}
#[test]
fn hex_full_range_odd() -> Result<()> {
let r = hex_range("abc");
assert_eq!(
r,
Some(HexSearch::Range(
hex::decode("abc0").expect("invalid hex"),
hex::decode("abd0").expect("invalid hex")
))
);
Ok(())
}
#[test]
fn hex_full_range_odd_end_f() -> Result<()> {
let r = hex_range("abf");
assert_eq!(
r,
Some(HexSearch::Range(
hex::decode("abf0").expect("invalid hex"),
hex::decode("ac00").expect("invalid hex")
))
);
Ok(())
}
#[test]
fn hex_no_upper() -> Result<()> {
let r = hex_range("ffff");
assert_eq!(
r,
Some(HexSearch::LowerOnly(
hex::decode("ffff").expect("invalid hex")
))
);
Ok(())
}
#[test]
fn hex_no_upper_odd() -> Result<()> {
let r = hex_range("fff");
assert_eq!(
r,
Some(HexSearch::LowerOnly(
hex::decode("fff0").expect("invalid hex")
))
);
Ok(())
}
}

View File

@@ -40,6 +40,8 @@ pub enum Error {
ConfigError(config::ConfigError),
#[error("Data directory does not exist")]
DatabaseDirError,
#[error("Custom Error : {0}")]
CustomError(String),
}
impl From<rusqlite::Error> for Error {

View File

@@ -9,6 +9,8 @@ use secp256k1::{schnorr, Secp256k1, VerifyOnly, XOnlyPublicKey};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::value::Value;
use serde_json::Number;
use std::collections::HashMap;
use std::collections::HashSet;
use std::str::FromStr;
use std::time::SystemTime;
@@ -35,6 +37,9 @@ pub struct Event {
pub(crate) tags: Vec<Vec<String>>,
pub(crate) content: String,
pub(crate) sig: String,
// Optimization for tag search, built on demand
#[serde(skip)]
pub(crate) tagidx: Option<HashMap<String, HashSet<String>>>,
}
/// Simple tag type for array of array of strings.
@@ -56,7 +61,9 @@ impl From<EventCmd> for Result<Event> {
if ec.cmd != "EVENT" {
Err(CommandUnknownError)
} else if ec.event.is_valid() {
Ok(ec.event)
let mut e = ec.event;
e.build_index();
Ok(e)
} else {
Err(EventInvalid)
}
@@ -72,6 +79,30 @@ fn unix_time() -> u64 {
}
impl Event {
/// Build an event tag index
fn build_index(&mut self) {
// if there are no tags; just leave the index as None
if self.tags.is_empty() {
return;
}
// otherwise, build an index
let mut idx: HashMap<String, HashSet<String>> = HashMap::new();
// iterate over tags that have at least 2 elements
for t in self.tags.iter().filter(|x| x.len() > 1) {
let tagname = t.get(0).unwrap();
let tagval = t.get(1).unwrap();
// ensure a vector exists for this tag
if !idx.contains_key(tagname) {
idx.insert(tagname.clone(), HashSet::new());
}
// get the tag vec and insert entry
let tidx = idx.get_mut(tagname).expect("could not get tag vector");
tidx.insert(tagval.clone());
}
// save the tag structure
self.tagidx = Some(idx);
}
/// Create a short event identifier, suitable for logging.
pub fn get_event_id_prefix(&self) -> String {
self.id.chars().take(8).collect()
@@ -114,6 +145,7 @@ impl Event {
return false;
}
// * validate the message digest (sig) using the pubkey & computed sha256 message hash.
let sig = schnorr::Signature::from_str(&self.sig).unwrap();
if let Ok(msg) = secp256k1::Message::from_slice(digest.as_ref()) {
let pubkey = XOnlyPublicKey::from_str(&self.pubkey).unwrap();
@@ -162,36 +194,18 @@ impl Event {
serde_json::Value::Array(tags)
}
/// Get a list of event tags.
pub fn get_event_tags(&self) -> Vec<&str> {
let mut etags = vec![];
for t in self.tags.iter() {
if t.len() >= 2 && t.get(0).unwrap() == "e" {
etags.push(&t.get(1).unwrap()[..]);
}
/// Determine if the given tag and value set intersect with tags in this event.
pub fn generic_tag_val_intersect(&self, tagname: &str, check: &HashSet<String>) -> bool {
match &self.tagidx {
Some(idx) => match idx.get(tagname) {
Some(valset) => {
let common = valset.intersection(check);
common.count() > 0
}
None => false,
},
None => false,
}
etags
}
/// Get a list of pubkey/petname tags.
pub fn get_pubkey_tags(&self) -> Vec<&str> {
let mut ptags = vec![];
for t in self.tags.iter() {
if t.len() >= 2 && t.get(0).unwrap() == "p" {
ptags.push(&t.get(1).unwrap()[..]);
}
}
ptags
}
/// Check if a given event is referenced in an event tag.
pub fn event_tag_match(&self, eventid: &str) -> bool {
self.get_event_tags().contains(&eventid)
}
/// Check if a given event is referenced in an event tag.
pub fn pubkey_tag_match(&self, pubkey: &str) -> bool {
self.get_pubkey_tags().contains(&pubkey)
}
}
@@ -207,6 +221,7 @@ mod tests {
tags: vec![],
content: "".to_owned(),
sig: "0".to_owned(),
tagidx: None,
}
}
@@ -229,7 +244,8 @@ mod tests {
#[test]
fn empty_event_tag_match() -> Result<()> {
let event = simple_event();
assert!(!event.event_tag_match("foo"));
assert!(!event
.generic_tag_val_intersect("e", &HashSet::from(["foo".to_owned(), "bar".to_owned()])));
Ok(())
}
@@ -237,7 +253,14 @@ mod tests {
fn single_event_tag_match() -> Result<()> {
let mut event = simple_event();
event.tags = vec![vec!["e".to_owned(), "foo".to_owned()]];
assert!(event.event_tag_match("foo"));
event.build_index();
assert_eq!(
event.generic_tag_val_intersect(
"e",
&HashSet::from(["foo".to_owned(), "bar".to_owned()])
),
true
);
Ok(())
}
@@ -281,6 +304,7 @@ mod tests {
tags: vec![],
content: "this is a test".to_owned(),
sig: "abcde".to_owned(),
tagidx: None,
};
let c = e.to_canonical();
let expected = Some(r#"[0,"012345",501234,1,[],"this is a test"]"#.to_owned());
@@ -304,6 +328,7 @@ mod tests {
],
content: "this is a test".to_owned(),
sig: "abcde".to_owned(),
tagidx: None,
};
let c = e.to_canonical();
let expected_json = r###"[0,"012345",501234,1,[["#e","aoeu"],["#p","aaaa","ws://example.com"]],"this is a test"]"###;

42
src/info.rs Normal file
View File

@@ -0,0 +1,42 @@
use crate::config;
/// Relay Info
use serde::{Deserialize, Serialize};
const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
#[derive(Debug, Serialize, Deserialize)]
#[allow(unused)]
pub struct RelayInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pubkey: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contact: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supported_nips: Option<Vec<i64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub software: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
/// Convert an Info configuration into public Relay Info
impl From<config::Info> for RelayInfo {
fn from(i: config::Info) -> Self {
RelayInfo {
id: i.relay_url,
name: i.name,
description: i.description,
pubkey: i.pubkey,
contact: i.contact,
supported_nips: Some(vec![1, 2, 11]),
software: Some("https://git.sr.ht/~gheartsfield/nostr-rs-relay".to_owned()),
version: CARGO_PKG_VERSION.map(|x| x.to_owned()),
}
}
}

View File

@@ -4,5 +4,7 @@ pub mod conn;
pub mod db;
pub mod error;
pub mod event;
pub mod info;
pub mod protostream;
pub mod subscription;
pub mod tags;

View File

@@ -1,6 +1,7 @@
//! Server process
use futures::SinkExt;
use futures::StreamExt;
use hyper::header::ACCEPT;
use hyper::service::{make_service_fn, service_fn};
use hyper::upgrade::Upgraded;
use hyper::{
@@ -13,6 +14,7 @@ use nostr_rs_relay::conn;
use nostr_rs_relay::db;
use nostr_rs_relay::error::{Error, Result};
use nostr_rs_relay::event::Event;
use nostr_rs_relay::info::RelayInfo;
use nostr_rs_relay::protostream;
use nostr_rs_relay::protostream::NostrMessage::*;
use nostr_rs_relay::protostream::NostrResponse::*;
@@ -30,14 +32,18 @@ use tokio_tungstenite::WebSocketStream;
use tungstenite::handshake;
use tungstenite::protocol::WebSocketConfig;
/// Return a requested DB name from command line arguments.
fn db_from_args(args: Vec<String>) -> Option<String> {
if args.len() == 3 && args.get(1) == Some(&"--db".to_owned()) {
return args.get(2).map(|x| x.to_owned());
}
None
}
/// Handle arbitrary HTTP requests, including for WebSocket upgrades.
async fn handle_web_request(
mut request: Request<Body>,
pool: db::SqlitePool,
remote_addr: SocketAddr,
broadcast: Sender<Event>,
event_tx: tokio::sync::mpsc::Sender<Event>,
@@ -47,7 +53,7 @@ async fn handle_web_request(
request.uri().path(),
request.headers().contains_key(header::UPGRADE),
) {
//if the request is ws_echo and the request headers contains an Upgrade key
// Request for / as websocket
("/", true) => {
debug!("websocket with upgrade request");
//assume request is a handshake, so create the handshake response
@@ -62,17 +68,25 @@ async fn handle_web_request(
match upgrade::on(&mut request).await {
//if successfully upgraded
Ok(upgraded) => {
// set WebSocket configuration options
let mut config = WebSocketConfig::default();
{
let settings = config::SETTINGS.read().unwrap();
config.max_message_size = settings.limits.max_ws_message_bytes;
config.max_frame_size = settings.limits.max_ws_frame_bytes;
}
//create a websocket stream from the upgraded object
let ws_stream = WebSocketStream::from_raw_socket(
//pass the upgraded object
//as the base layer stream of the Websocket
upgraded,
tokio_tungstenite::tungstenite::protocol::Role::Server,
None,
Some(config),
)
.await;
tokio::spawn(nostr_server(
ws_stream, broadcast, event_tx, shutdown,
pool, ws_stream, broadcast, event_tx, shutdown,
));
}
Err(e) => println!(
@@ -96,10 +110,30 @@ async fn handle_web_request(
};
Ok::<_, Infallible>(response)
}
// Request for Relay info
("/", false) => {
// handle request at root with no upgrade header
// Check if this is a nostr server info request
let accept_header = &request.headers().get(ACCEPT);
// check if application/nostr+json is included
if let Some(media_types) = accept_header {
if let Ok(mt_str) = media_types.to_str() {
if mt_str.contains("application/nostr+json") {
let config = config::SETTINGS.read().unwrap();
// build a relay info response
debug!("Responding to server info request");
let rinfo = RelayInfo::from(config.info.clone());
let b = Body::from(serde_json::to_string_pretty(&rinfo).unwrap());
return Ok(Response::builder()
.status(200)
.header("Content-Type", "application/nostr+json")
.body(b)
.unwrap());
}
}
}
Ok(Response::new(Body::from(
"This is a Nostr relay.\n".to_string(),
"Please use a Nostr client to connect.",
)))
}
(_, _) => {
@@ -143,9 +177,15 @@ fn main() -> Result<(), Error> {
error!("Database directory does not exist");
return Err(Error::DatabaseDirError);
}
debug!("config: {:?}", config);
trace!("config: {:?}", config);
let addr = format!("{}:{}", config.network.address.trim(), config.network.port);
let socket_addr = addr.parse().expect("listening address not valid");
if let Some(addr_whitelist) = &config.authorization.pubkey_whitelist {
info!(
"Event publishing restricted to {} pubkey(s)",
addr_whitelist.len()
);
}
// configure tokio runtime
let rt = Builder::new_multi_thread()
.enable_all()
@@ -167,21 +207,24 @@ fn main() -> Result<(), Error> {
// establish a channel for letting all threads now about a
// requested server shutdown.
let (invoke_shutdown, _) = broadcast::channel::<()>(1);
let ctrl_c_shutdown = invoke_shutdown.clone();
// // listen for ctrl-c interruupts
tokio::spawn(async move {
tokio::signal::ctrl_c().await.unwrap();
info!("shutting down due to SIGINT");
ctrl_c_shutdown.send(()).ok();
});
// start the database writer thread. Give it a channel for
// writing events, and for publishing events that have been
// written (to all connected clients).
db::db_writer(event_rx, bcast_tx.clone(), invoke_shutdown.subscribe()).await;
info!("db writer created");
// // listen for ctrl-c interruupts
let ctrl_c_shutdown = invoke_shutdown.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.unwrap();
info!("shutting down due to SIGINT");
ctrl_c_shutdown.send(()).ok();
});
// build a connection pool for sqlite connections
let pool = db::build_read_pool();
// A `Service` is needed for every connection, so this
// creates one from our `handle_request` function.
let make_svc = make_service_fn(|conn: &AddrStream| {
let svc_pool = pool.clone();
let remote_addr = conn.remote_addr();
let bcast = bcast_tx.clone();
let event = event_tx.clone();
@@ -191,6 +234,7 @@ fn main() -> Result<(), Error> {
Ok::<_, Infallible>(service_fn(move |request: Request<Body>| {
handle_web_request(
request,
svc_pool.clone(),
remote_addr,
bcast.clone(),
event.clone(),
@@ -214,6 +258,7 @@ fn main() -> Result<(), Error> {
/// Handle new client connections. This runs through an event loop
/// for all client communication.
async fn nostr_server(
pool: db::SqlitePool,
ws_stream: WebSocketStream<Upgraded>,
broadcast: Sender<Event>,
event_tx: tokio::sync::mpsc::Sender<Event>,
@@ -221,12 +266,6 @@ async fn nostr_server(
) {
// get a broadcast channel for clients to communicate on
let mut bcast_rx = broadcast.subscribe();
let mut config = WebSocketConfig::default();
{
let settings = config::SETTINGS.read().unwrap();
config.max_message_size = settings.limits.max_ws_message_bytes;
config.max_frame_size = settings.limits.max_ws_frame_bytes;
}
// upgrade the TCP connection to WebSocket
//let conn = tokio_tungstenite::accept_async_with_config(stream, Some(config)).await;
//let ws_stream = conn.expect("websocket handshake error");
@@ -311,7 +350,9 @@ async fn nostr_server(
Ok(()) => {
running_queries.insert(s.id.to_owned(), abandon_query_tx);
// start a database query
db::db_query(s, query_tx.clone(), abandon_query_rx).await;
// show pool stats
debug!("DB pool stats: {:?}", pool.state());
db::db_query(s, pool.get().expect("could not get connection"), query_tx.clone(), abandon_query_rx).await;
},
Err(e) => {
info!("Subscription error: {}", e);

View File

@@ -71,6 +71,7 @@ impl Stream for NostrStream {
}
Err(e) => {
debug!("proto parse error: {:?}", e);
debug!("parse error on message: {}", msg.trim());
Err(Error::ProtoParseError)
}
}
@@ -80,8 +81,14 @@ impl Stream for NostrStream {
Poll::Ready(None) => Poll::Ready(None),
Poll::Ready(Some(v)) => match v {
Ok(Message::Text(vs)) => Poll::Ready(Some(convert(vs))),
Ok(Message::Ping(_x)) => {
debug!("client ping");
//Pin::new(&mut self.ws_stream).start_send(Message::Pong(x));
//info!("sent pong");
Poll::Pending
}
Ok(Message::Binary(_)) => Poll::Ready(Some(Err(Error::ProtoParseError))),
Ok(Message::Pong(_)) | Ok(Message::Ping(_)) => Poll::Pending,
Ok(Message::Pong(_)) => Poll::Pending,
Ok(Message::Close(_)) => Poll::Ready(None),
Err(WsError::AlreadyClosed) | Err(WsError::ConnectionClosed) => Poll::Ready(None),
Err(_) => Poll::Ready(Some(Err(Error::ConnError))),

View File

@@ -1,7 +1,11 @@
//! Subscription and filter parsing
use crate::error::Result;
use crate::event::Event;
use serde::de::Unexpected;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::collections::HashSet;
/// Subscription identifier and set of request filters
#[derive(Serialize, PartialEq, Debug, Clone)]
@@ -15,24 +19,76 @@ pub struct Subscription {
/// Corresponds to client-provided subscription request elements. Any
/// element can be present if it should be used in filtering, or
/// absent ([`None`]) if it should be ignored.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[derive(Serialize, PartialEq, Debug, Clone)]
pub struct ReqFilter {
/// Event hash
pub id: Option<String>,
/// Event kind
pub kind: Option<u64>,
/// Referenced event hash
#[serde(rename = "#e")]
pub event: Option<String>,
/// Referenced public key for a petname
#[serde(rename = "#p")]
pub pubkey: Option<String>,
/// Event hashes
pub ids: Option<Vec<String>>,
/// Event kinds
pub kinds: Option<Vec<u64>>,
/// Events published after this time
pub since: Option<u64>,
/// Events published before this time
pub until: Option<u64>,
/// List of author public keys
pub authors: Option<Vec<String>>,
/// Set of tags
#[serde(skip)]
pub tags: Option<HashMap<String, HashSet<String>>>,
}
impl<'de> Deserialize<'de> for ReqFilter {
fn deserialize<D>(deserializer: D) -> Result<ReqFilter, D::Error>
where
D: Deserializer<'de>,
{
let received: Value = Deserialize::deserialize(deserializer)?;
let filter = received.as_object().ok_or_else(|| {
serde::de::Error::invalid_type(
Unexpected::Other("reqfilter is not an object"),
&"a json object",
)
})?;
let mut rf = ReqFilter {
ids: None,
kinds: None,
since: None,
until: None,
authors: None,
tags: None,
};
let mut ts = None;
// iterate through each key, and assign values that exist
for (key, val) in filter.into_iter() {
// ids
if key == "ids" {
rf.ids = Deserialize::deserialize(val).ok();
} else if key == "kinds" {
rf.kinds = Deserialize::deserialize(val).ok();
} else if key == "since" {
rf.since = Deserialize::deserialize(val).ok();
} else if key == "until" {
rf.until = Deserialize::deserialize(val).ok();
} else if key == "authors" {
rf.authors = Deserialize::deserialize(val).ok();
} else if key.starts_with('#') && key.len() > 1 && val.is_array() {
// remove the prefix
let tagname = &key[1..];
if ts.is_none() {
// Initialize the tag if necessary
ts = Some(HashMap::new());
}
if let Some(m) = ts.as_mut() {
let tag_vals: Option<Vec<String>> = Deserialize::deserialize(val).ok();
if let Some(v) = tag_vals {
let hs = HashSet::from_iter(v.into_iter());
m.insert(tagname.to_owned(), hs);
}
};
}
}
rf.tags = ts;
Ok(rf)
}
}
impl<'de> Deserialize<'de> for Subscription {
@@ -42,7 +98,7 @@ impl<'de> Deserialize<'de> for Subscription {
where
D: Deserializer<'de>,
{
let mut v: serde_json::Value = Deserialize::deserialize(deserializer)?;
let mut v: Value = Deserialize::deserialize(deserializer)?;
// this shoud be a 3-or-more element array.
// verify the first element is a String, REQ
// get the subscription from the second element.
@@ -77,6 +133,7 @@ impl<'de> Deserialize<'de> for Subscription {
for fv in i {
let f: ReqFilter = serde_json::from_value(fv.take())
.map_err(|_| serde::de::Error::custom("could not parse filter"))?;
// create indexes
filters.push(f);
}
Ok(Subscription {
@@ -103,46 +160,63 @@ impl Subscription {
}
}
impl ReqFilter {
/// Check for a match within the authors list.
// TODO: Ambiguity; what if the array is empty? Should we
// consider that the same as null?
fn authors_match(&self, event: &Event) -> bool {
self.authors
.as_ref()
.map(|vs| vs.contains(&event.pubkey.to_owned()))
.unwrap_or(true)
fn prefix_match(prefixes: &[String], target: &str) -> bool {
for prefix in prefixes {
if target.starts_with(prefix) {
return true;
}
}
/// Check if this filter either matches, or does not care about the event tags.
fn event_match(&self, event: &Event) -> bool {
self.event
// none matched
false
}
impl ReqFilter {
fn ids_match(&self, event: &Event) -> bool {
self.ids
.as_ref()
.map(|t| event.event_tag_match(t))
.map(|vs| prefix_match(vs, &event.id))
.unwrap_or(true)
}
/// Check if this filter either matches, or does not care about
/// the pubkey/petname tags.
fn pubkey_match(&self, event: &Event) -> bool {
self.pubkey
fn authors_match(&self, event: &Event) -> bool {
self.authors
.as_ref()
.map(|t| event.pubkey_tag_match(t))
.map(|vs| prefix_match(vs, &event.pubkey))
.unwrap_or(true)
}
fn tag_match(&self, event: &Event) -> bool {
// get the hashset from the filter.
if let Some(map) = &self.tags {
for (key, val) in map.iter() {
let tag_match = event.generic_tag_val_intersect(key, val);
// if there is no match for this tag, the match fails.
if !tag_match {
return false;
}
// if there was a match, we move on to the next one.
}
}
// if the tag map is empty, the match succeeds (there was no filter)
true
}
/// Check if this filter either matches, or does not care about the kind.
fn kind_match(&self, kind: u64) -> bool {
self.kind.map(|v| v == kind).unwrap_or(true)
self.kinds
.as_ref()
.map(|ks| ks.contains(&kind))
.unwrap_or(true)
}
/// Determine if all populated fields in this filter match the provided event.
pub fn interested_in_event(&self, event: &Event) -> bool {
self.id.as_ref().map(|v| v == &event.id).unwrap_or(true)
// self.id.as_ref().map(|v| v == &event.id).unwrap_or(true)
self.ids_match(event)
&& self.since.map(|t| event.created_at > t).unwrap_or(true)
&& self.kind_match(event.kind)
&& self.authors_match(event)
&& self.pubkey_match(event)
&& self.event_match(event)
&& self.tag_match(event)
}
}
@@ -173,27 +247,66 @@ mod tests {
}
#[test]
fn invalid_filter() {
// unrecognized field in filter
let raw_json = "[\"REQ\",\"some-id\",{\"foo\": 3}]";
assert!(serde_json::from_str::<Subscription>(raw_json).is_err());
fn legacy_filter() {
// legacy field in filter
let raw_json = "[\"REQ\",\"some-id\",{\"kind\": 3}]";
assert!(serde_json::from_str::<Subscription>(raw_json).is_ok());
}
#[test]
fn author_filter() -> Result<()> {
let raw_json = "[\"REQ\",\"some-id\",{\"author\": \"test-author-id\"}]";
let raw_json = r#"["REQ","some-id",{"authors": ["test-author-id"]}]"#;
let s: Subscription = serde_json::from_str(raw_json)?;
assert_eq!(s.id, "some-id");
assert_eq!(s.filters.len(), 1);
let first_filter = s.filters.get(0).unwrap();
assert_eq!(first_filter.author, Some("test-author-id".to_owned()));
assert_eq!(
first_filter.authors,
Some(vec!("test-author-id".to_owned()))
);
Ok(())
}
#[test]
fn interest_author_prefix_match() -> Result<()> {
// subscription with a filter for ID
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"authors": ["abc"]}]"#)?;
let e = Event {
id: "foo".to_owned(),
pubkey: "abcd".to_owned(),
created_at: 0,
kind: 0,
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert!(s.interested_in_event(&e));
Ok(())
}
#[test]
fn interest_id_prefix_match() -> Result<()> {
// subscription with a filter for ID
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"ids": ["abc"]}]"#)?;
let e = Event {
id: "abcd".to_owned(),
pubkey: "".to_owned(),
created_at: 0,
kind: 0,
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert!(s.interested_in_event(&e));
Ok(())
}
#[test]
fn interest_id_nomatch() -> Result<()> {
// subscription with a filter for ID
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"id":"abc"}]"#)?;
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"ids": ["xyz"]}]"#)?;
let e = Event {
id: "abcde".to_owned(),
pubkey: "".to_owned(),
@@ -202,15 +315,17 @@ mod tests {
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert_eq!(s.interested_in_event(&e), false);
assert!(!s.interested_in_event(&e));
Ok(())
}
#[test]
fn interest_time_and_id() -> Result<()> {
// subscription with a filter for ID and time
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"id":"abc", "since": 1000}]"#)?;
let s: Subscription =
serde_json::from_str(r#"["REQ","xyz",{"ids": ["abc"], "since": 1000}]"#)?;
let e = Event {
id: "abc".to_owned(),
pubkey: "".to_owned(),
@@ -219,8 +334,9 @@ mod tests {
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert_eq!(s.interested_in_event(&e), false);
assert!(!s.interested_in_event(&e));
Ok(())
}
@@ -236,8 +352,9 @@ mod tests {
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert_eq!(s.interested_in_event(&e), true);
assert!(s.interested_in_event(&e));
Ok(())
}
@@ -253,8 +370,9 @@ mod tests {
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert_eq!(s.interested_in_event(&e), true);
assert!(s.interested_in_event(&e));
Ok(())
}
@@ -270,8 +388,9 @@ mod tests {
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert_eq!(s.interested_in_event(&e), true);
assert!(s.interested_in_event(&e));
Ok(())
}
#[test]
@@ -287,8 +406,9 @@ mod tests {
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert_eq!(s.interested_in_event(&e), true);
assert!(s.interested_in_event(&e));
Ok(())
}
@@ -304,8 +424,9 @@ mod tests {
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert_eq!(s.interested_in_event(&e), false);
assert!(!s.interested_in_event(&e));
Ok(())
}
}

315
src/tags.rs Normal file
View File

@@ -0,0 +1,315 @@
//! Tags used in events to link to another event or a pubkey
//!
//! Reference specification NIP01: https://github.com/fiatjaf/nostr/blob/master/nips/01.md#events-and-signatures
//!
use bitcoin_hashes::hex::ToHex;
use bitcoin_hashes::sha256;
use secp256k1::XOnlyPublicKey;
use std::str::FromStr;
use serde::de::Unexpected;
use serde::ser::SerializeSeq;
use serde::{Deserialize, Deserializer, Serializer};
use serde_json::Value;
use crate::error::Error;
type EventId = sha256::Hash;
#[derive(Debug, PartialEq)]
struct EventTag {
event_id: EventId,
recommended_url: Option<String>,
}
#[derive(Debug, PartialEq)]
struct PubkeyTag {
pubkey: XOnlyPublicKey,
recommended_url: Option<String>,
}
// Tag structure representing two possible types of tags
#[derive(Debug, PartialEq)]
enum Tag {
Event(EventTag),
Pubkey(PubkeyTag),
}
// Custom json serialization into protocol network format
// Event tag : ["e", "<32 byte event-id>", "optional<url>"]
// Pubkey tag : ["p", "<32 byte Xonly Pubkey>", "optional<url>"]
impl serde::Serialize for Tag {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Event(event_tag) => {
let mut seq = serializer.serialize_seq(None)?;
seq.serialize_element("e")?;
seq.serialize_element(&event_tag.event_id.to_hex())?;
if let Some(url) = &event_tag.recommended_url {
seq.serialize_element(url)?;
} else {
}
seq.end()
}
Self::Pubkey(pubkey_tag) => {
let mut seq = serializer.serialize_seq(None)?;
seq.serialize_element("p")?;
seq.serialize_element(&pubkey_tag.pubkey.to_hex())?;
if let Some(url) = &pubkey_tag.recommended_url {
seq.serialize_element(url)?;
} else {
}
seq.end()
}
}
}
}
// Custom json deserialization from protocol network format
impl<'de> serde::Deserialize<'de> for Tag {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// Receive incoming data in a json object
let received: Value = Deserialize::deserialize(deserializer)?;
// Check received data is a json array
let values = received.as_array().ok_or_else(|| {
serde::de::Error::invalid_type(Unexpected::Other("tag json object"), &"json array")
})?;
// Check json array contains only string
let values = values
.iter()
.map(|value| {
value.as_str().ok_or_else(|| {
serde::de::Error::invalid_type(
Unexpected::Other("tag json data"),
&"json string",
)
})
})
.collect::<Result<Vec<_>, _>>()?;
// Check length is not more tha 3
if values.len() > 3 {
Err(serde::de::Error::invalid_length(
values.len(),
&"tag length is 2 or 3",
))
} else {
// Parse the json array into appropriate types
match values[0] {
// This denotes an event type tag
"e" => {
let event_id = EventId::from_str(values[1])
.map_err(|e| serde::de::Error::custom(e.to_string()))?;
let recomended_url = if values.len() == 3 {
Some(values[2].into())
} else {
None
};
Ok(Tag::Event(EventTag {
event_id,
recommended_url: recomended_url,
}))
}
// This denotes a pubkey type tag
"p" => {
let pubkey = XOnlyPublicKey::from_str(values[1])
.map_err(|e| serde::de::Error::custom(e.to_string()))?;
let recomended_url = if values.len() == 3 {
Some(values[2].into())
} else {
None
};
Ok(Tag::Pubkey(PubkeyTag {
pubkey,
recommended_url: recomended_url,
}))
}
// Any other tag type is currently not supported
_ => Err(serde::de::Error::invalid_value(
Unexpected::Other("tag type flag"),
&"'e' or 'p'",
)),
}
}
}
}
#[allow(dead_code)]
// Some api (not public currently) to use Tags
impl Tag {
fn new_event_tag(event_id: EventId, recomended_url: Option<String>) -> Self {
Self::Event(EventTag {
event_id,
recommended_url: recomended_url,
})
}
fn new_pubkey_tag(pubkey: XOnlyPublicKey, recomended_url: Option<String>) -> Self {
Self::Pubkey(PubkeyTag {
pubkey,
recommended_url: recomended_url,
})
}
fn get_recomended_url(&self) -> Option<String> {
match self {
Self::Event(EventTag {
event_id: _,
recommended_url,
}) => recommended_url.clone(),
Self::Pubkey(PubkeyTag {
pubkey: _,
recommended_url,
}) => recommended_url.clone(),
}
}
fn get_event_id(&self) -> Result<EventId, Error> {
match self {
Self::Event(EventTag {
event_id,
recommended_url: _,
}) => Ok(*event_id),
_ => Err(Error::CustomError("Expected event tag".to_string())),
}
}
fn get_pubkey(&self) -> Result<XOnlyPublicKey, Error> {
match self {
Self::Pubkey(PubkeyTag {
pubkey,
recommended_url: _,
}) => Ok(*pubkey),
_ => Err(Error::CustomError("Expected pubkey tag".to_string())),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use bitcoin_hashes::Hash;
#[test]
fn serde_roundtrip() {
let pubkey = XOnlyPublicKey::from_str(
"18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166",
)
.unwrap();
let url = "wss://rsslay.fiatjaf.com";
static HASH_BYTES: [u8; 32] = [
0xef, 0x53, 0x7f, 0x25, 0xc8, 0x95, 0xbf, 0xa7, 0x82, 0x52, 0x65, 0x29, 0xa9, 0xb6,
0x3d, 0x97, 0xaa, 0x63, 0x15, 0x64, 0xd5, 0xd7, 0x89, 0xc2, 0xb7, 0x65, 0x44, 0x8c,
0x86, 0x35, 0xfb, 0x6c,
];
let event_id = EventId::from_slice(&HASH_BYTES).expect("right number of bytes");
let event_tag = Tag::new_event_tag(event_id, Some(url.to_string()));
let pubkey_tag = Tag::new_pubkey_tag(pubkey, Some(url.to_string()));
let ser_event_tag = serde_json::to_string(&event_tag).unwrap();
let ser_pubkey_tag = serde_json::to_string(&pubkey_tag).unwrap();
let deser_event_tag: Tag = serde_json::from_str(&ser_event_tag).unwrap();
let deser_pubkey_tag: Tag = serde_json::from_str(&ser_pubkey_tag).unwrap();
assert_eq!(deser_event_tag, event_tag);
assert_eq!(deser_pubkey_tag, pubkey_tag);
}
#[test]
fn invalid_pubkey() {
let test_string = r#"["p","845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166","wss://rsslay.fiatjaf.com"]"#;
let tag: Result<Tag, _> = serde_json::from_str(test_string);
assert_eq!(
tag.err().expect("expect error").to_string(),
"secp: malformed public key".to_string()
);
}
#[test]
fn invalid_datatype() {
let test_string = r#"["p", 188457, "wss://rsslay.fiatjaf.com"]"#;
let tag: Result<Tag, _> = serde_json::from_str(test_string);
assert_eq!(
tag.err().expect("expect error").to_string(),
"invalid type: tag json data, expected json string".to_string()
);
}
#[test]
fn invalid_tagbyte() {
let test_string = r#"["q", "18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166", "wss://rsslay.fiatjaf.com"]"#;
let tag: Result<Tag, _> = serde_json::from_str(test_string);
assert_eq!(
tag.err().expect("expect error").to_string(),
"invalid value: tag type flag, expected 'e' or 'p'".to_string()
);
}
#[test]
fn invalid_event_id() {
let test_string = r#"["e","ef537f25c895bfa782526529a9b63d97aa631564d5d78c8635fb6c","wss://rsslay.fiatjaf.com"]"#;
let tag: Result<Tag, _> = serde_json::from_str(test_string);
assert_eq!(
tag.err().expect("expect error").to_string(),
"bad hex string length 54 (expected 64)".to_string()
);
}
#[test]
fn invalid_url_type() {
let test_string = r#"["e","ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c", 123456788]"#;
let tag: Result<Tag, _> = serde_json::from_str(test_string);
assert_eq!(
tag.err().expect("expect error").to_string(),
"invalid type: tag json data, expected json string".to_string()
);
}
#[test]
fn invalid_length() {
let test_string = r#"["e","ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c","wss://rsslay.fiatjaf.com", "Random extra data"]"#;
let tag: Result<Tag, _> = serde_json::from_str(test_string);
assert_eq!(
tag.err().expect("expect error").to_string(),
"invalid length 4, expected tag length is 2 or 3".to_string()
);
}
#[test]
fn invalid_json_type() {
let test_string = r#"{"type": "e","event": "ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c"}"#;
let tag: Result<Tag, _> = serde_json::from_str(test_string);
assert_eq!(
tag.err().expect("expect error").to_string(),
"invalid type: tag json object, expected json array".to_string()
);
}
#[test]
fn evenrt_tag_missing_url() {
let test_string =
r#"["e","ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c"]"#;
let tag: Tag = serde_json::from_str(test_string).unwrap();
assert!(tag.get_recomended_url().is_none());
}
#[test]
fn pubkey_tag_missing_url() {
let test_string =
r#"["p","18845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166"]"#;
let tag: Tag = serde_json::from_str(test_string).unwrap();
assert!(tag.get_recomended_url().is_none());
}
}