Compare commits

...
35 Commits
Author SHA1 Message Date
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
Greg Heartsfield 850957213e build: bump version to 0.2.3 2022-01-01 09:13:13 -06:00
Greg Heartsfield 1aa5a5458d improvement: event signature validation is 100x faster
Switched to latest (git) release of secp256k1, which has more
efficient verification-only context for Schnorr.  Switched to single
pre-instantiated instance of the verifier.
2022-01-01 09:08:19 -06:00
Greg Heartsfield 620e227699 fix: connection issues with Firefox
This adds Hyper, and a 200 response code.  Prior to this, Firefox
would fail to connect.  There is also a text document displayed at the
root URL to indicate this is a Nostr relay.

Fixes https://todo.sr.ht/~gheartsfield/nostr-rs-relay/15
2022-01-01 08:11:20 -06:00
15 changed files with 1638 additions and 322 deletions
Generated
+276 -98
View File
@@ -63,9 +63,15 @@ version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce18265ec2324ad075345d5814fbeed4f41f0a660055dc78840b74d19b874b1" checksum = "7ce18265ec2324ad075345d5814fbeed4f41f0a660055dc78840b74d19b874b1"
dependencies = [ dependencies = [
"serde 1.0.131", "serde 1.0.136",
] ]
[[package]]
name = "bitcoin_hashes"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "006cc91e1a1d99819bc5b8214be3555c1f0611b169f527a1fdc54ed1f2b745b0"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@@ -83,9 +89,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.8.0" version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
@@ -129,7 +135,7 @@ dependencies = [
"lazy_static", "lazy_static",
"nom", "nom",
"rust-ini", "rust-ini",
"serde 1.0.131", "serde 1.0.136",
"serde-hjson", "serde-hjson",
"serde_json", "serde_json",
"toml", "toml",
@@ -147,9 +153,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.5" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"lazy_static", "lazy_static",
@@ -157,13 +163,12 @@ dependencies = [
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "5.0.0" version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b799062aaf67eb976af3bdca031ee6f846d2f0a5710ddbb0d2efee33f3cc4760" checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"num_cpus", "num_cpus",
"parking_lot",
] ]
[[package]] [[package]]
@@ -224,9 +229,9 @@ checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e" checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@@ -239,9 +244,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27" checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
@@ -249,15 +254,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7"
[[package]] [[package]]
name = "futures-executor" name = "futures-executor"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97" checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
@@ -266,15 +271,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11" checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2"
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd" checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -283,15 +288,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af" checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72"
[[package]] [[package]]
name = "futures-timer" name = "futures-timer"
@@ -301,9 +306,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.18" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@@ -319,9 +324,9 @@ dependencies = [
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.4" version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
dependencies = [ dependencies = [
"typenum", "typenum",
"version_check", "version_check",
@@ -329,9 +334,9 @@ dependencies = [
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.3" version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
@@ -340,9 +345,9 @@ dependencies = [
[[package]] [[package]]
name = "governor" name = "governor"
version = "0.4.0" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3de427a64787873c3b196285e6684cddbf0ae7d1d8d56eaafbb4120c4cb641" checksum = "7df0ee4b237afb71e99f7e2fbd840ffec2d6c4bb569f69b2af18aa1f63077d38"
dependencies = [ dependencies = [
"dashmap", "dashmap",
"futures", "futures",
@@ -355,6 +360,25 @@ dependencies = [
"smallvec", "smallvec",
] ]
[[package]]
name = "h2"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9de88456263e249e241fcd211d3954e2c9b0ef7ccfc235a444eb367cae3689"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.11.2" version = "0.11.2"
@@ -390,13 +414,24 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
"itoa", "itoa 1.0.1",
]
[[package]]
name = "http-body"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
dependencies = [
"bytes",
"http",
"pin-project-lite",
] ]
[[package]] [[package]]
@@ -405,12 +440,42 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
[[package]]
name = "httpdate"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]] [[package]]
name = "humantime" name = "humantime"
version = "2.1.0" version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa 0.4.8",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.2.3" version = "0.2.3"
@@ -422,6 +487,16 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "indexmap"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
dependencies = [
"autocfg 1.0.1",
"hashbrown",
]
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.12" version = "0.1.12"
@@ -438,10 +513,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]] [[package]]
name = "js-sys" name = "itoa"
version = "0.3.55" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
@@ -467,9 +548,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.111" version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e167738f1866a7ec625567bae89ca0d44477232a4f7c52b1c7f2adc2c98804f" checksum = "b0005d08a8f7b65fb8073cb697aa0b12b631ed251ce73d862ce50eeb52ce3b50"
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
@@ -573,21 +654,24 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
[[package]] [[package]]
name = "nostr-rs-relay" name = "nostr-rs-relay"
version = "0.2.2" version = "0.4.0"
dependencies = [ dependencies = [
"bitcoin_hashes", "bitcoin_hashes 0.9.7",
"config", "config",
"env_logger", "env_logger",
"futures", "futures",
"futures-util", "futures-util",
"governor", "governor",
"hex", "hex",
"hyper",
"lazy_static", "lazy_static",
"log", "log",
"nonzero_ext", "nonzero_ext",
"r2d2",
"r2d2_sqlite",
"rusqlite", "rusqlite",
"secp256k1", "secp256k1",
"serde 1.0.131", "serde 1.0.136",
"serde_json", "serde_json",
"thiserror", "thiserror",
"tokio", "tokio",
@@ -625,9 +709,9 @@ dependencies = [
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.13.0" version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi",
"libc", "libc",
@@ -635,9 +719,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.8.0" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
@@ -678,9 +762,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.7" version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@@ -696,15 +780,15 @@ checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.15" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.33" version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb37d2df5df740e582f28f8560cf425f52bb267d872fe58358eadb554909f07a" checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
dependencies = [ dependencies = [
"unicode-xid", "unicode-xid",
] ]
@@ -727,13 +811,34 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.10" version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
dependencies = [ dependencies = [
"proc-macro2", "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]] [[package]]
name = "rand" name = "rand"
version = "0.6.5" version = "0.6.5"
@@ -947,9 +1052,18 @@ checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.7" version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "scopeguard" name = "scopeguard"
@@ -960,20 +1074,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]] [[package]]
name = "secp256k1" name = "secp256k1"
version = "0.20.3" version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/rust-bitcoin/rust-secp256k1.git?rev=50034ccb18fdd84904ab3aa6c84a12fcced33209#50034ccb18fdd84904ab3aa6c84a12fcced33209"
checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a"
dependencies = [ dependencies = [
"bitcoin_hashes", "bitcoin_hashes 0.10.0",
"rand 0.6.5", "rand 0.6.5",
"secp256k1-sys", "secp256k1-sys",
"serde 1.0.131", "serde 1.0.136",
] ]
[[package]] [[package]]
name = "secp256k1-sys" name = "secp256k1-sys"
version = "0.4.1" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/rust-bitcoin/rust-secp256k1.git?rev=50034ccb18fdd84904ab3aa6c84a12fcced33209#50034ccb18fdd84904ab3aa6c84a12fcced33209"
checksum = "827cb7cce42533829c792fc51b82fbf18b125b45a702ef2c8be77fce65463a7b"
dependencies = [ dependencies = [
"cc", "cc",
] ]
@@ -986,9 +1098,9 @@ checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.131" version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1" checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@@ -1007,9 +1119,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.131" version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b710a83c4e0dff6a3d511946b95274ad9ca9e5d3ae497b63fda866ac955358d2" checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1018,13 +1130,14 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.72" version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085"
dependencies = [ dependencies = [
"itoa", "indexmap",
"itoa 1.0.1",
"ryu", "ryu",
"serde 1.0.131", "serde 1.0.136",
] ]
[[package]] [[package]]
@@ -1057,9 +1170,19 @@ checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.7.0" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
[[package]]
name = "socket2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f82496b90c36d70af5fcd482edaa2e0bd16fade569de1330405fecbbdac736b"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "static_assertions" name = "static_assertions"
@@ -1069,9 +1192,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.82" version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1124,11 +1247,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.14.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838"
dependencies = [ dependencies = [
"autocfg 1.0.1",
"bytes", "bytes",
"libc", "libc",
"memchr", "memchr",
@@ -1144,9 +1266,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "1.6.0" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1165,15 +1287,61 @@ dependencies = [
"tungstenite", "tungstenite",
] ]
[[package]]
name = "tokio-util"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"log",
"pin-project-lite",
"tokio",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.5.8" version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
dependencies = [ dependencies = [
"serde 1.0.131", "serde 1.0.136",
] ]
[[package]]
name = "tower-service"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
[[package]]
name = "tracing"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
dependencies = [
"cfg-if",
"pin-project-lite",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
dependencies = [
"lazy_static",
]
[[package]]
name = "try-lock"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.16.0" version = "0.16.0"
@@ -1195,9 +1363,9 @@ dependencies = [
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.14.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
@@ -1255,9 +1423,19 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.3" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "want"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
dependencies = [
"log",
"try-lock",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
@@ -1267,9 +1445,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.78" version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"wasm-bindgen-macro", "wasm-bindgen-macro",
@@ -1277,9 +1455,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.78" version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"lazy_static", "lazy_static",
@@ -1292,9 +1470,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.78" version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -1302,9 +1480,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.78" version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1315,15 +1493,15 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.78" version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2"
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.55" version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
+7 -4
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "nostr-rs-relay" name = "nostr-rs-relay"
version = "0.2.2" version = "0.4.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@@ -15,11 +15,14 @@ thiserror = "^1"
uuid = { version = "^0.8", features = ["v4"] } uuid = { version = "^0.8", features = ["v4"] }
config = { version = "0.11", features = ["toml"] } config = { version = "0.11", features = ["toml"] }
bitcoin_hashes = { version = "^0.9", features = ["serde"] } bitcoin_hashes = { version = "^0.9", features = ["serde"] }
secp256k1 = { version = "^0.20", features = ["rand", "rand-std", "serde", "bitcoin_hashes"] } 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 = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0" serde_json = {version = "^1.0", features = ["preserve_order"]}
hex = "^0.4" hex = "^0.4"
rusqlite = "^0.26" rusqlite = { version = "^0.26", features = ["limits"]}
r2d2 = "^0.8"
r2d2_sqlite = "^0.19"
lazy_static = "^1.4" lazy_static = "^1.4"
governor = "^0.4" governor = "^0.4"
nonzero_ext = "^0.3" nonzero_ext = "^0.3"
hyper={ version="0.14", features=["server","http1","http2","tcp"] }
+25 -1
View File
@@ -8,6 +8,17 @@ The project master repository is available on
[sourcehut](https://sr.ht/~gheartsfield/nostr-rs-relay/), and is [sourcehut](https://sr.ht/~gheartsfield/nostr-rs-relay/), and is
mirrored on [GitHub](https://github.com/scsibug/nostr-rs-relay). mirrored on [GitHub](https://github.com/scsibug/nostr-rs-relay).
## Features
NIPs with relay-specific implementation requirements are listed here.
- [x] NIP-01: Core event model
- [x] NIP-01: Hide old metadata events
- [x] NIP-02: Hide old contact list events
- [ ] NIP-03: OpenTimestamps
- [ ] NIP-09: Event deletion
- [x] NIP-11: Relay information document
## Quick Start ## Quick Start
The provided `Dockerfile` will compile and build the server The provided `Dockerfile` will compile and build the server
@@ -39,9 +50,12 @@ Text Note [81cf...2652] from 296a...9b92 5 seconds ago
hello world hello world
``` ```
A pre-built container is also available on DockerHub:
https://hub.docker.com/r/scsibug/nostr-rs-relay
## Configuration ## 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 configuration available to the relay. This file is optional, but may
be mounted into a docker container like so: be mounted into a docker container like so:
@@ -55,6 +69,16 @@ $ docker run -it -p 7000:8080 \
Options include rate-limiting, event size limits, and network address Options include rate-limiting, event size limits, and network address
settings. 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 License
--- ---
This project is MIT licensed. This project is MIT licensed.
+32 -6
View File
@@ -1,13 +1,39 @@
# Nostr-rs-relay configuration # 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] [database]
# Directory for SQLite files. Defaults to the current directory. Can # Directory for SQLite files. Defaults to the current directory. Can
# also be specified (and overriden) with the "--db dirname" command # also be specified (and overriden) with the "--db dirname" command
# line option. # line option.
data_directory = "." 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] [network]
# Bind to this network address # Bind to this network address
address = "0.0.0.0" address = "0.0.0.0"
# Listen on this port # Listen on this port
port = 8080 port = 8080
@@ -20,22 +46,22 @@ reject_future_seconds = 1800
[limits] [limits]
# Limit events created per second, averaged over one minute. Must be # Limit events created per second, averaged over one minute. Must be
# an integer. If not set (or set to 0), defaults to unlimited. # 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. # Limit the maximum size of an EVENT message. Defaults to 128 KB.
# Set to 0 for unlimited. # Set to 0 for unlimited.
max_event_bytes = 131072 #max_event_bytes = 131072
# Maximum WebSocket message in bytes. Defaults to 128 KB. # 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. # 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 # Broadcast buffer size, in number of events. This prevents slow
# readers from consuming memory. Defaults to 4096. # readers from consuming memory. Defaults to 4096.
broadcast_buffer = 4096 #broadcast_buffer = 4096
# Event persistence buffer size, in number of events. This provides # Event persistence buffer size, in number of events. This provides
# backpressure to senders if writes are slow. Defaults to 16. # backpressure to senders if writes are slow. Defaults to 16.
event_persist_buffer = 16 #event_persist_buffer = 16
+53
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.
+29
View File
@@ -8,10 +8,22 @@ lazy_static! {
pub static ref SETTINGS: RwLock<Settings> = RwLock::new(Settings::default()); 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)] #[derive(Debug, Serialize, Deserialize)]
#[allow(unused)] #[allow(unused)]
pub struct Database { pub struct Database {
pub data_directory: String, pub data_directory: String,
pub min_conn: u32,
pub max_conn: u32,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@@ -52,6 +64,7 @@ pub struct Limits {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[allow(unused)] #[allow(unused)]
pub struct Settings { pub struct Settings {
pub info: Info,
pub database: Database, pub database: Database,
pub network: Network, pub network: Network,
pub limits: Limits, pub limits: Limits,
@@ -82,6 +95,13 @@ impl Settings {
// override with file contents // override with file contents
.with_merged(config::File::with_name("config"))? .with_merged(config::File::with_name("config"))?
.try_into()?; .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) Ok(settings)
} }
} }
@@ -89,8 +109,17 @@ impl Settings {
impl Default for Settings { impl Default for Settings {
fn default() -> Self { fn default() -> Self {
Settings { Settings {
info: Info {
relay_url: None,
name: Some("Unnamed nostr-rs-relay".to_owned()),
description: None,
pubkey: None,
contact: None,
},
database: Database { database: Database {
data_directory: ".".to_owned(), data_directory: ".".to_owned(),
min_conn: 4,
max_conn: 128,
}, },
network: Network { network: Network {
port: 8080, port: 8080,
+435 -87
View File
@@ -1,4 +1,5 @@
//! Event persistence and querying //! Event persistence and querying
use crate::config;
use crate::error::Result; use crate::error::Result;
use crate::event::Event; use crate::event::Event;
use crate::subscription::Subscription; use crate::subscription::Subscription;
@@ -11,10 +12,17 @@ use rusqlite::Connection;
use rusqlite::OpenFlags; use rusqlite::OpenFlags;
//use std::num::NonZeroU32; //use std::num::NonZeroU32;
use crate::config::SETTINGS; use crate::config::SETTINGS;
use r2d2;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::limits::Limit;
use rusqlite::types::ToSql;
use std::path::Path; use std::path::Path;
use std::thread; use std::thread;
use std::time::Instant;
use tokio::task; use tokio::task;
pub type SqlitePool = r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>;
/// Database file /// Database file
const DB_FILE: &str = "nostr.db"; const DB_FILE: &str = "nostr.db";
@@ -33,7 +41,7 @@ PRAGMA journal_mode=WAL;
PRAGMA main.synchronous=NORMAL; PRAGMA main.synchronous=NORMAL;
PRAGMA foreign_keys = ON; PRAGMA foreign_keys = ON;
PRAGMA application_id = 1654008667; PRAGMA application_id = 1654008667;
PRAGMA user_version = 2; PRAGMA user_version = 3;
-- Event Table -- Event Table
CREATE TABLE IF NOT EXISTS event ( CREATE TABLE IF NOT EXISTS event (
@@ -53,6 +61,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 author_index ON event(author);
CREATE INDEX IF NOT EXISTS kind_index ON event(kind); 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 -- Event References Table
CREATE TABLE IF NOT EXISTS event_ref ( CREATE TABLE IF NOT EXISTS event_ref (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
@@ -76,22 +99,56 @@ 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); CREATE INDEX IF NOT EXISTS pubkey_ref_index ON pubkey_ref(referenced_pubkey);
"##; "##;
pub fn build_read_pool() -> SqlitePool {
info!("Build a connection pool");
let config = config::SETTINGS.read().unwrap();
let db_dir = &config.database.data_directory;
let full_path = Path::new(db_dir).join(DB_FILE);
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();
return pool;
}
/// Upgrade DB to latest version, and execute pragma settings /// Upgrade DB to latest version, and execute pragma settings
pub fn upgrade_db(conn: &mut Connection) -> Result<()> { pub fn upgrade_db(conn: &mut Connection) -> Result<()> {
// check the version. // check the version.
let curr_version = db_version(conn)?; let mut curr_version = db_version(conn)?;
info!("DB version = {:?}", curr_version); 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 // initialize from scratch
if curr_version == 0 { if curr_version == 0 {
match conn.execute_batch(INIT_SQL) { 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) => { Err(err) => {
error!("update failed: {}", err); error!("update failed: {}", err);
panic!("database could not be initialized"); panic!("database could not be initialized");
} }
} }
} else if curr_version == 1 { }
if curr_version == 1 {
// only change is adding a hidden column to events. // only change is adding a hidden column to events.
let upgrade_sql = r##" let upgrade_sql = r##"
ALTER TABLE event ADD hidden INTEGER; ALTER TABLE event ADD hidden INTEGER;
@@ -99,19 +156,73 @@ UPDATE event SET hidden=FALSE;
PRAGMA user_version = 2; PRAGMA user_version = 2;
"##; "##;
match conn.execute_batch(upgrade_sql) { 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) => { Err(err) => {
error!("update failed: {}", err); error!("update failed: {}", err);
panic!("database could not be upgraded"); 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"); 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"); panic!("Database version is newer than supported by this executable");
} }
// Setup PRAGMA // Setup PRAGMA
conn.execute_batch(STARTUP_SQL)?; conn.execute_batch(STARTUP_SQL)?;
info!("Finished pragma");
Ok(()) Ok(())
} }
@@ -133,8 +244,10 @@ pub async fn db_writer(
)?; )?;
info!("opened database {:?} for writing", full_path); info!("opened database {:?} for writing", full_path);
upgrade_db(&mut conn)?; upgrade_db(&mut conn)?;
// get rate limit settings // get rate limit settings
let rps_setting = config.limits.messages_per_sec; let rps_setting = config.limits.messages_per_sec;
let mut most_recent_rate_limit = Instant::now();
let mut lim_opt = None; let mut lim_opt = None;
let clock = governor::clock::QuantaClock::default(); let clock = governor::clock::QuantaClock::default();
if let Some(rps) = rps_setting { if let Some(rps) = rps_setting {
@@ -157,12 +270,17 @@ pub async fn db_writer(
} }
let mut event_write = false; let mut event_write = false;
let event = next_event.unwrap(); let event = next_event.unwrap();
let start = Instant::now();
match write_event(&mut conn, &event) { match write_event(&mut conn, &event) {
Ok(updated) => { Ok(updated) => {
if updated == 0 { if updated == 0 {
debug!("ignoring duplicate event"); debug!("ignoring duplicate event");
} else { } else {
info!("persisted event: {}", event.get_event_id_prefix()); info!(
"persisted event: {} in {:?}",
event.get_event_id_prefix(),
start.elapsed()
);
event_write = true; event_write = true;
// send this out to all clients // send this out to all clients
bcast_tx.send(event.clone()).ok(); bcast_tx.send(event.clone()).ok();
@@ -176,8 +294,20 @@ pub async fn db_writer(
if event_write { if event_write {
if let Some(ref lim) = lim_opt { if let Some(ref lim) = lim_opt {
if let Err(n) = lim.check() { if let Err(n) = lim.check() {
info!("Rate limiting event creation"); let wait_for = n.wait_time_from(clock.now());
thread::sleep(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; continue;
} }
} }
@@ -215,24 +345,24 @@ pub fn write_event(conn: &mut Connection, e: &Event) -> Result<usize> {
} }
// remember primary key of the event most recently inserted. // remember primary key of the event most recently inserted.
let ev_id = tx.last_insert_rowid(); let ev_id = tx.last_insert_rowid();
// add all event tags into the event_ref table // add all tags to the tag table
let etags = e.get_event_tags(); for tag in e.tags.iter() {
if !etags.is_empty() { // ensure we have 2 values.
for etag in etags.iter() { if tag.len() >= 2 {
tx.execute( let tagname = &tag[0];
"INSERT OR IGNORE INTO event_ref (event_id, referenced_event) VALUES (?1, ?2)", let tagval = &tag[1];
params![ev_id, hex::decode(&etag).ok()], // if tagvalue is hex;
)?; if is_hex(tagval) {
} tx.execute(
} "INSERT OR IGNORE INTO tag (event_id, name, value_hex) VALUES (?1, ?2, ?3)",
// add all event tags into the pubkey_ref table params![ev_id, &tagname, hex::decode(&tagval).ok()],
let ptags = e.get_pubkey_tags(); )?;
if !ptags.is_empty() { } else {
for ptag in ptags.iter() { tx.execute(
tx.execute( "INSERT OR IGNORE INTO tag (event_id, name, value) VALUES (?1, ?2, ?3)",
"INSERT OR IGNORE INTO pubkey_ref (event_id, referenced_pubkey) VALUES (?1, ?2)", params![ev_id, &tagname, &tagval],
params![ev_id, hex::decode(&ptag).ok()], )?;
)?; }
} }
} }
// if this event is for a metadata update, hide every other kind=0 // if this event is for a metadata update, hide every other kind=0
@@ -275,60 +405,194 @@ fn is_hex(s: &str) -> bool {
s.chars().all(|x| char::is_ascii_hexdigit(&x)) s.chars().all(|x| char::is_ascii_hexdigit(&x))
} }
/// Create a dynamic SQL query string from a subscription. /// Check if a string contains only f chars
fn query_from_sub(sub: &Subscription) -> String { 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 // build a dynamic SQL query. all user-input is either an integer
// (sqli-safe), or a string that is filtered to only contain // (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 = 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 " "SELECT DISTINCT(e.content) FROM event e LEFT JOIN tag t ON e.id=t.event_id ".to_owned();
.to_owned(); // parameters
let mut params: Vec<Box<dyn ToSql>> = vec![];
// for every filter in the subscription, generate a where clause // for every filter in the subscription, generate a where clause
let mut filter_clauses: Vec<String> = Vec::new(); let mut filter_clauses: Vec<String> = Vec::new();
for f in sub.filters.iter() { for f in sub.filters.iter() {
// individual filter components // individual filter components
let mut filter_components: Vec<String> = Vec::new(); let mut filter_components: Vec<String> = Vec::new();
// Query for "authors" // Query for "authors", allowing prefix matches
if f.authors.is_some() { if let Some(authvec) = &f.authors {
let authors_escaped: Vec<String> = f // take each author and convert to a hexsearch
.authors let mut auth_searches: Vec<String> = vec![];
.as_ref() for auth in authvec {
.unwrap() match hex_range(auth) {
.iter() Some(HexSearch::Exact(ex)) => {
.filter(|&x| is_hex(x)) info!("Exact match for author");
.map(|x| format!("x'{}'", x)) auth_searches.push("author=?".to_owned());
.collect(); params.push(Box::new(ex));
let authors_clause = format!("author IN ({})", authors_escaped.join(", ")); }
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 {:?}", auth);
}
}
}
let authors_clause = format!("({})", auth_searches.join(" OR "));
filter_components.push(authors_clause); filter_components.push(authors_clause);
} }
// Query for Kind // Query for Kind
if f.kind.is_some() { if let Some(ks) = &f.kinds {
// kind is number, no escaping needed // 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); filter_components.push(kind_clause);
} }
// Query for event // Query for event, allowing prefix matches
if f.id.is_some() { if let Some(idvec) = &f.ids {
let id_str = f.id.as_ref().unwrap(); // take each author and convert to a hexsearch
if is_hex(id_str) { let mut id_searches: Vec<String> = vec![];
let id_clause = format!("event_hash = x'{}'", id_str); for id in idvec {
filter_components.push(id_clause); 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);
}
}
} }
let id_clause = format!("({})", id_searches.join(" OR "));
filter_components.push(id_clause);
} }
// Query for referenced event // Query for tags
if f.event.is_some() { if let Some(map) = &f.tags {
let ev_str = f.event.as_ref().unwrap(); for (key, val) in map.iter() {
if is_hex(ev_str) { let mut str_vals: Vec<Box<dyn ToSql>> = vec![];
let ev_clause = format!("referenced_event = x'{}'", ev_str); let mut blob_vals: Vec<Box<dyn ToSql>> = vec![];
filter_components.push(ev_clause); for v in val {
} if is_hex(v) {
} if let Ok(h) = hex::decode(&v) {
// Query for referenced pet name pubkey blob_vals.push(Box::new(h));
if f.pubkey.is_some() { }
let pet_str = f.pubkey.as_ref().unwrap(); } else {
if is_hex(pet_str) { str_vals.push(Box::new(v.to_owned()));
let pet_clause = format!("referenced_pubkey = x'{}'", pet_str); }
filter_components.push(pet_clause); }
// 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 // Query for timestamp
@@ -348,21 +612,22 @@ fn query_from_sub(sub: &Subscription) -> String {
fc.push_str(&filter_components.join(" AND ")); fc.push_str(&filter_components.join(" AND "));
fc.push_str(" )"); fc.push_str(" )");
filter_clauses.push(fc); 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 // combine all filters with OR clauses, if any exist
if !filter_clauses.is_empty() { if !filter_clauses.is_empty() {
query.push_str(" WHERE "); query.push_str(" AND (");
query.push_str(&filter_clauses.join(" OR ")); query.push_str(&filter_clauses.join(" OR "));
query.push_str(") ");
} }
// add order clause // add order clause
query.push_str(" ORDER BY created_at ASC"); query.push_str(" ORDER BY created_at ASC");
debug!("query string: {}", query); debug!("query string: {}", query);
query (query, params)
} }
/// Perform a database query using a subscription. /// Perform a database query using a subscription.
@@ -373,31 +638,27 @@ fn query_from_sub(sub: &Subscription) -> String {
/// query is immediately aborted. /// query is immediately aborted.
pub async fn db_query( pub async fn db_query(
sub: Subscription, sub: Subscription,
conn: r2d2::PooledConnection<r2d2_sqlite::SqliteConnectionManager>,
query_tx: tokio::sync::mpsc::Sender<QueryResult>, query_tx: tokio::sync::mpsc::Sender<QueryResult>,
mut abandon_query_rx: tokio::sync::oneshot::Receiver<()>, mut abandon_query_rx: tokio::sync::oneshot::Receiver<()>,
) { ) {
task::spawn_blocking(move || { 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); debug!("going to query for: {:?}", sub);
let mut row_count: usize = 0;
let start = Instant::now();
// generate SQL query // generate SQL query
let q = query_from_sub(&sub); let (q, p) = query_from_sub(&sub);
// execute the query // execute the query. Don't cache, since queries vary so much.
let mut stmt = conn.prepare(&q).unwrap(); let mut stmt = conn.prepare(&q)?;
let mut event_rows = stmt.query([]).unwrap(); let mut event_rows = stmt.query(rusqlite::params_from_iter(p))?;
while let Some(row) = event_rows.next().unwrap() { while let Some(row) = event_rows.next()? {
// check if this is still active (we could do this every N rows) // check if this is still active (we could do this every N rows)
if abandon_query_rx.try_recv().is_ok() { if abandon_query_rx.try_recv().is_ok() {
debug!("query aborted"); debug!("query aborted");
return; return Ok(());
} }
// TODO: check before unwrapping row_count += 1;
let event_json = row.get(0).unwrap(); let event_json = row.get(0)?;
query_tx query_tx
.blocking_send(QueryResult { .blocking_send(QueryResult {
sub_id: sub.get_id(), sub_id: sub.get_id(),
@@ -405,6 +666,93 @@ pub async fn db_query(
}) })
.ok(); .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(())
}
}
+2
View File
@@ -40,6 +40,8 @@ pub enum Error {
ConfigError(config::ConfigError), ConfigError(config::ConfigError),
#[error("Data directory does not exist")] #[error("Data directory does not exist")]
DatabaseDirError, DatabaseDirError,
#[error("Custom Error : {0}")]
CustomError(String),
} }
impl From<rusqlite::Error> for Error { impl From<rusqlite::Error> for Error {
+72 -39
View File
@@ -3,14 +3,21 @@ use crate::config;
use crate::error::Error::*; use crate::error::Error::*;
use crate::error::Result; use crate::error::Result;
use bitcoin_hashes::{sha256, Hash}; use bitcoin_hashes::{sha256, Hash};
use lazy_static::lazy_static;
use log::*; use log::*;
use secp256k1::{schnorrsig, Secp256k1}; use secp256k1::{schnorr, Secp256k1, VerifyOnly, XOnlyPublicKey};
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use serde_json::value::Value; use serde_json::value::Value;
use serde_json::Number; use serde_json::Number;
use std::collections::HashMap;
use std::collections::HashSet;
use std::str::FromStr; use std::str::FromStr;
use std::time::SystemTime; use std::time::SystemTime;
lazy_static! {
pub static ref SECP: Secp256k1<VerifyOnly> = Secp256k1::verification_only();
}
/// Event command in network format /// Event command in network format
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct EventCmd { pub struct EventCmd {
@@ -30,6 +37,9 @@ pub struct Event {
pub(crate) tags: Vec<Vec<String>>, pub(crate) tags: Vec<Vec<String>>,
pub(crate) content: String, pub(crate) content: String,
pub(crate) sig: 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. /// Simple tag type for array of array of strings.
@@ -51,7 +61,9 @@ impl From<EventCmd> for Result<Event> {
if ec.cmd != "EVENT" { if ec.cmd != "EVENT" {
Err(CommandUnknownError) Err(CommandUnknownError)
} else if ec.event.is_valid() { } else if ec.event.is_valid() {
Ok(ec.event) let mut e = ec.event;
e.build_index();
Ok(e)
} else { } else {
Err(EventInvalid) Err(EventInvalid)
} }
@@ -67,6 +79,30 @@ fn unix_time() -> u64 {
} }
impl Event { 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. /// Create a short event identifier, suitable for logging.
pub fn get_event_id_prefix(&self) -> String { pub fn get_event_id_prefix(&self) -> String {
self.id.chars().take(8).collect() self.id.chars().take(8).collect()
@@ -109,12 +145,16 @@ impl Event {
return false; return false;
} }
// * validate the message digest (sig) using the pubkey & computed sha256 message hash. // * validate the message digest (sig) using the pubkey & computed sha256 message hash.
let secp = Secp256k1::new();
let sig = schnorrsig::Signature::from_str(&self.sig).unwrap(); let sig = schnorr::Signature::from_str(&self.sig).unwrap();
let message = secp256k1::Message::from(digest); if let Ok(msg) = secp256k1::Message::from_slice(digest.as_ref()) {
let pubkey = schnorrsig::PublicKey::from_str(&self.pubkey).unwrap(); let pubkey = XOnlyPublicKey::from_str(&self.pubkey).unwrap();
let verify = secp.schnorrsig_verify(&sig, &message, &pubkey); let verify = SECP.verify_schnorr(&sig, &msg, &pubkey);
matches!(verify, Ok(())) matches!(verify, Ok(()))
} else {
warn!("Error converting digest to secp256k1 message");
false
}
} }
/// Convert event to canonical representation for signing. /// Convert event to canonical representation for signing.
@@ -154,36 +194,18 @@ impl Event {
serde_json::Value::Array(tags) serde_json::Value::Array(tags)
} }
/// Get a list of event tags. /// Determine if the given tag and value set intersect with tags in this event.
pub fn get_event_tags(&self) -> Vec<&str> { pub fn generic_tag_val_intersect(&self, tagname: &str, check: &HashSet<String>) -> bool {
let mut etags = vec![]; match &self.tagidx {
for t in self.tags.iter() { Some(idx) => match idx.get(tagname) {
if t.len() >= 2 && t.get(0).unwrap() == "e" { Some(valset) => {
etags.push(&t.get(1).unwrap()[..]); 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)
} }
} }
@@ -199,6 +221,7 @@ mod tests {
tags: vec![], tags: vec![],
content: "".to_owned(), content: "".to_owned(),
sig: "0".to_owned(), sig: "0".to_owned(),
tagidx: None,
} }
} }
@@ -221,7 +244,8 @@ mod tests {
#[test] #[test]
fn empty_event_tag_match() -> Result<()> { fn empty_event_tag_match() -> Result<()> {
let event = simple_event(); 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(()) Ok(())
} }
@@ -229,7 +253,14 @@ mod tests {
fn single_event_tag_match() -> Result<()> { fn single_event_tag_match() -> Result<()> {
let mut event = simple_event(); let mut event = simple_event();
event.tags = vec![vec!["e".to_owned(), "foo".to_owned()]]; 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(()) Ok(())
} }
@@ -273,6 +304,7 @@ mod tests {
tags: vec![], tags: vec![],
content: "this is a test".to_owned(), content: "this is a test".to_owned(),
sig: "abcde".to_owned(), sig: "abcde".to_owned(),
tagidx: None,
}; };
let c = e.to_canonical(); let c = e.to_canonical();
let expected = Some(r#"[0,"012345",501234,1,[],"this is a test"]"#.to_owned()); let expected = Some(r#"[0,"012345",501234,1,[],"this is a test"]"#.to_owned());
@@ -296,6 +328,7 @@ mod tests {
], ],
content: "this is a test".to_owned(), content: "this is a test".to_owned(),
sig: "abcde".to_owned(), sig: "abcde".to_owned(),
tagidx: None,
}; };
let c = e.to_canonical(); let c = e.to_canonical();
let expected_json = r###"[0,"012345",501234,1,[["#e","aoeu"],["#p","aaaa","ws://example.com"]],"this is a test"]"###; let expected_json = r###"[0,"012345",501234,1,[["#e","aoeu"],["#p","aaaa","ws://example.com"]],"this is a test"]"###;
+42
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()),
}
}
}
+2
View File
@@ -4,5 +4,7 @@ pub mod conn;
pub mod db; pub mod db;
pub mod error; pub mod error;
pub mod event; pub mod event;
pub mod info;
pub mod protostream; pub mod protostream;
pub mod subscription; pub mod subscription;
pub mod tags;
+167 -34
View File
@@ -1,6 +1,12 @@
//! Server process //! Server process
use futures::SinkExt; use futures::SinkExt;
use futures::StreamExt; use futures::StreamExt;
use hyper::header::ACCEPT;
use hyper::service::{make_service_fn, service_fn};
use hyper::upgrade::Upgraded;
use hyper::{
header, server::conn::AddrStream, upgrade, Body, Request, Response, Server, StatusCode,
};
use log::*; use log::*;
use nostr_rs_relay::close::Close; use nostr_rs_relay::close::Close;
use nostr_rs_relay::config; use nostr_rs_relay::config;
@@ -8,20 +14,25 @@ use nostr_rs_relay::conn;
use nostr_rs_relay::db; use nostr_rs_relay::db;
use nostr_rs_relay::error::{Error, Result}; use nostr_rs_relay::error::{Error, Result};
use nostr_rs_relay::event::Event; use nostr_rs_relay::event::Event;
use nostr_rs_relay::info::RelayInfo;
use nostr_rs_relay::protostream; use nostr_rs_relay::protostream;
use nostr_rs_relay::protostream::NostrMessage::*; use nostr_rs_relay::protostream::NostrMessage::*;
use nostr_rs_relay::protostream::NostrResponse::*; use nostr_rs_relay::protostream::NostrResponse::*;
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::Infallible;
use std::env; use std::env;
use std::net::SocketAddr;
use std::path::Path; use std::path::Path;
use tokio::net::{TcpListener, TcpStream};
use tokio::runtime::Builder; use tokio::runtime::Builder;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tokio::sync::broadcast::{Receiver, Sender}; use tokio::sync::broadcast::{Receiver, Sender};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tokio_tungstenite::WebSocketStream;
use tungstenite::handshake;
use tungstenite::protocol::WebSocketConfig; use tungstenite::protocol::WebSocketConfig;
/// Return a requested DB name from command line arguments.
fn db_from_args(args: Vec<String>) -> Option<String> { fn db_from_args(args: Vec<String>) -> Option<String> {
if args.len() == 3 && args.get(1) == Some(&"--db".to_owned()) { if args.len() == 3 && args.get(1) == Some(&"--db".to_owned()) {
return args.get(2).map(|x| x.to_owned()); return args.get(2).map(|x| x.to_owned());
@@ -29,6 +40,119 @@ fn db_from_args(args: Vec<String>) -> Option<String> {
None 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>,
shutdown: Receiver<()>,
) -> Result<Response<Body>, Infallible> {
match (
request.uri().path(),
request.headers().contains_key(header::UPGRADE),
) {
// Request for / as websocket
("/", true) => {
debug!("websocket with upgrade request");
//assume request is a handshake, so create the handshake response
let response = match handshake::server::create_response_with_body(&request, || {
Body::empty()
}) {
Ok(response) => {
//in case the handshake response creation succeeds,
//spawn a task to handle the websocket connection
tokio::spawn(async move {
//using the hyper feature of upgrading a connection
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,
Some(config),
)
.await;
tokio::spawn(nostr_server(
pool, ws_stream, broadcast, event_tx, shutdown,
));
}
Err(e) => println!(
"error when trying to upgrade connection \
from address {} to websocket connection. \
Error is: {}",
remote_addr, e
),
}
});
//return the response to the handshake request
response
}
Err(error) => {
warn!("websocket response failed");
let mut res =
Response::new(Body::from(format!("Failed to create websocket: {}", error)));
*res.status_mut() = StatusCode::BAD_REQUEST;
return Ok(res);
}
};
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(
"Please use a Nostr client to connect.",
)))
}
(_, _) => {
//handle any other url
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("Nothing here."))
.unwrap())
}
}
}
async fn shutdown_signal() {
// Wait for the CTRL+C signal
tokio::signal::ctrl_c()
.await
.expect("failed to install CTRL+C signal handler");
}
/// Start running a Nostr relay server. /// Start running a Nostr relay server.
fn main() -> Result<(), Error> { fn main() -> Result<(), Error> {
// setup logger // setup logger
@@ -46,6 +170,7 @@ fn main() -> Result<(), Error> {
} }
*settings = c; *settings = c;
} }
let config = config::SETTINGS.read().unwrap(); let config = config::SETTINGS.read().unwrap();
// do some config validation. // do some config validation.
if !Path::new(&config.database.data_directory).is_dir() { if !Path::new(&config.database.data_directory).is_dir() {
@@ -54,6 +179,7 @@ fn main() -> Result<(), Error> {
} }
debug!("config: {:?}", config); debug!("config: {:?}", config);
let addr = format!("{}:{}", config.network.address.trim(), config.network.port); let addr = format!("{}:{}", config.network.address.trim(), config.network.port);
let socket_addr = addr.parse().expect("listening address not valid");
// configure tokio runtime // configure tokio runtime
let rt = Builder::new_multi_thread() let rt = Builder::new_multi_thread()
.enable_all() .enable_all()
@@ -63,8 +189,9 @@ fn main() -> Result<(), Error> {
// start tokio // start tokio
rt.block_on(async { rt.block_on(async {
let settings = config::SETTINGS.read().unwrap(); let settings = config::SETTINGS.read().unwrap();
let listener = TcpListener::bind(&addr).await.expect("Failed to bind"); info!("listening on: {}", socket_addr);
info!("listening on: {}", addr); // build a connection pool for sqlite connections
let pool = db::build_read_pool();
// all client-submitted valid events are broadcast to every // all client-submitted valid events are broadcast to every
// other client on this channel. This should be large enough // other client on this channel. This should be large enough
// to accomodate slower readers (messages are dropped if // to accomodate slower readers (messages are dropped if
@@ -77,7 +204,7 @@ fn main() -> Result<(), Error> {
// requested server shutdown. // requested server shutdown.
let (invoke_shutdown, _) = broadcast::channel::<()>(1); let (invoke_shutdown, _) = broadcast::channel::<()>(1);
let ctrl_c_shutdown = invoke_shutdown.clone(); let ctrl_c_shutdown = invoke_shutdown.clone();
// listen for ctrl-c interruupts // // listen for ctrl-c interruupts
tokio::spawn(async move { tokio::spawn(async move {
tokio::signal::ctrl_c().await.unwrap(); tokio::signal::ctrl_c().await.unwrap();
info!("shutting down due to SIGINT"); info!("shutting down due to SIGINT");
@@ -87,28 +214,37 @@ fn main() -> Result<(), Error> {
// writing events, and for publishing events that have been // writing events, and for publishing events that have been
// written (to all connected clients). // written (to all connected clients).
db::db_writer(event_rx, bcast_tx.clone(), invoke_shutdown.subscribe()).await; db::db_writer(event_rx, bcast_tx.clone(), invoke_shutdown.subscribe()).await;
info!("db writer created");
// track unique client connection count // A `Service` is needed for every connection, so this
let mut client_accept_count: usize = 0; // creates one from our `handle_request` function.
let mut stop_listening = invoke_shutdown.subscribe(); let make_svc = make_service_fn(|conn: &AddrStream| {
// handle new client connection requests, or SIGINT signals. let svc_pool = pool.clone();
loop { let remote_addr = conn.remote_addr();
tokio::select! { let bcast = bcast_tx.clone();
_ = stop_listening.recv() => { let event = event_tx.clone();
break; let stop = invoke_shutdown.clone();
} async move {
Ok((stream, _)) = listener.accept() => { // service_fn converts our function into a `Service`
client_accept_count += 1; Ok::<_, Infallible>(service_fn(move |request: Request<Body>| {
info!("creating new connection for client #{}",client_accept_count); handle_web_request(
tokio::spawn(nostr_server( request,
stream, svc_pool.clone(),
bcast_tx.clone(), remote_addr,
event_tx.clone(), bcast.clone(),
invoke_shutdown.subscribe(), event.clone(),
)); stop.subscribe(),
} )
}))
} }
});
let server = Server::bind(&socket_addr)
.serve(make_svc)
.with_graceful_shutdown(shutdown_signal());
// run hyper
if let Err(e) = server.await {
eprintln!("server error: {}", e);
} }
// our code
}); });
Ok(()) Ok(())
} }
@@ -116,22 +252,17 @@ fn main() -> Result<(), Error> {
/// Handle new client connections. This runs through an event loop /// Handle new client connections. This runs through an event loop
/// for all client communication. /// for all client communication.
async fn nostr_server( async fn nostr_server(
stream: TcpStream, pool: db::SqlitePool,
ws_stream: WebSocketStream<Upgraded>,
broadcast: Sender<Event>, broadcast: Sender<Event>,
event_tx: tokio::sync::mpsc::Sender<Event>, event_tx: tokio::sync::mpsc::Sender<Event>,
mut shutdown: Receiver<()>, mut shutdown: Receiver<()>,
) { ) {
// get a broadcast channel for clients to communicate on // get a broadcast channel for clients to communicate on
let mut bcast_rx = broadcast.subscribe(); 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 // upgrade the TCP connection to WebSocket
let conn = tokio_tungstenite::accept_async_with_config(stream, Some(config)).await; //let conn = tokio_tungstenite::accept_async_with_config(stream, Some(config)).await;
let ws_stream = conn.expect("websocket handshake error"); //let ws_stream = conn.expect("websocket handshake error");
// wrap websocket into a stream & sink of Nostr protocol messages // wrap websocket into a stream & sink of Nostr protocol messages
let mut nostr_stream = protostream::wrap_ws_in_nostr(ws_stream); let mut nostr_stream = protostream::wrap_ws_in_nostr(ws_stream);
// Track internal client state // Track internal client state
@@ -213,7 +344,9 @@ async fn nostr_server(
Ok(()) => { Ok(()) => {
running_queries.insert(s.id.to_owned(), abandon_query_tx); running_queries.insert(s.id.to_owned(), abandon_query_tx);
// start a database query // 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) => { Err(e) => {
info!("Subscription error: {}", e); info!("Subscription error: {}", e);
+11 -4
View File
@@ -9,9 +9,9 @@ use futures::sink::Sink;
use futures::stream::Stream; use futures::stream::Stream;
use futures::task::Context; use futures::task::Context;
use futures::task::Poll; use futures::task::Poll;
use hyper::upgrade::Upgraded;
use log::*; use log::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::net::TcpStream;
use tokio_tungstenite::WebSocketStream; use tokio_tungstenite::WebSocketStream;
use tungstenite::error::Error as WsError; use tungstenite::error::Error as WsError;
use tungstenite::protocol::Message; use tungstenite::protocol::Message;
@@ -40,11 +40,11 @@ pub enum NostrResponse {
/// A Nostr protocol stream is layered on top of a Websocket stream. /// A Nostr protocol stream is layered on top of a Websocket stream.
pub struct NostrStream { pub struct NostrStream {
ws_stream: WebSocketStream<TcpStream>, ws_stream: WebSocketStream<Upgraded>,
} }
/// Given a websocket, return a protocol stream wrapper. /// Given a websocket, return a protocol stream wrapper.
pub fn wrap_ws_in_nostr(ws: WebSocketStream<TcpStream>) -> NostrStream { pub fn wrap_ws_in_nostr(ws: WebSocketStream<Upgraded>) -> NostrStream {
NostrStream { ws_stream: ws } NostrStream { ws_stream: ws }
} }
@@ -71,6 +71,7 @@ impl Stream for NostrStream {
} }
Err(e) => { Err(e) => {
debug!("proto parse error: {:?}", e); debug!("proto parse error: {:?}", e);
debug!("parse error on message: {}", msg.trim());
Err(Error::ProtoParseError) Err(Error::ProtoParseError)
} }
} }
@@ -80,8 +81,14 @@ impl Stream for NostrStream {
Poll::Ready(None) => Poll::Ready(None), Poll::Ready(None) => Poll::Ready(None),
Poll::Ready(Some(v)) => match v { Poll::Ready(Some(v)) => match v {
Ok(Message::Text(vs)) => Poll::Ready(Some(convert(vs))), 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::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), Ok(Message::Close(_)) => Poll::Ready(None),
Err(WsError::AlreadyClosed) | Err(WsError::ConnectionClosed) => Poll::Ready(None), Err(WsError::AlreadyClosed) | Err(WsError::ConnectionClosed) => Poll::Ready(None),
Err(_) => Poll::Ready(Some(Err(Error::ConnError))), Err(_) => Poll::Ready(Some(Err(Error::ConnError))),
+170 -49
View File
@@ -1,7 +1,11 @@
//! Subscription and filter parsing //! Subscription and filter parsing
use crate::error::Result; use crate::error::Result;
use crate::event::Event; use crate::event::Event;
use serde::de::Unexpected;
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::collections::HashSet;
/// Subscription identifier and set of request filters /// Subscription identifier and set of request filters
#[derive(Serialize, PartialEq, Debug, Clone)] #[derive(Serialize, PartialEq, Debug, Clone)]
@@ -15,24 +19,76 @@ pub struct Subscription {
/// Corresponds to client-provided subscription request elements. Any /// Corresponds to client-provided subscription request elements. Any
/// element can be present if it should be used in filtering, or /// element can be present if it should be used in filtering, or
/// absent ([`None`]) if it should be ignored. /// absent ([`None`]) if it should be ignored.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] #[derive(Serialize, PartialEq, Debug, Clone)]
pub struct ReqFilter { pub struct ReqFilter {
/// Event hash /// Event hashes
pub id: Option<String>, pub ids: Option<Vec<String>>,
/// Event kind /// Event kinds
pub kind: Option<u64>, pub kinds: Option<Vec<u64>>,
/// Referenced event hash
#[serde(rename = "#e")]
pub event: Option<String>,
/// Referenced public key for a petname
#[serde(rename = "#p")]
pub pubkey: Option<String>,
/// Events published after this time /// Events published after this time
pub since: Option<u64>, pub since: Option<u64>,
/// Events published before this time /// Events published before this time
pub until: Option<u64>, pub until: Option<u64>,
/// List of author public keys /// List of author public keys
pub authors: Option<Vec<String>>, 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 { impl<'de> Deserialize<'de> for Subscription {
@@ -42,7 +98,7 @@ impl<'de> Deserialize<'de> for Subscription {
where where
D: Deserializer<'de>, 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. // this shoud be a 3-or-more element array.
// verify the first element is a String, REQ // verify the first element is a String, REQ
// get the subscription from the second element. // get the subscription from the second element.
@@ -77,6 +133,7 @@ impl<'de> Deserialize<'de> for Subscription {
for fv in i { for fv in i {
let f: ReqFilter = serde_json::from_value(fv.take()) let f: ReqFilter = serde_json::from_value(fv.take())
.map_err(|_| serde::de::Error::custom("could not parse filter"))?; .map_err(|_| serde::de::Error::custom("could not parse filter"))?;
// create indexes
filters.push(f); filters.push(f);
} }
Ok(Subscription { Ok(Subscription {
@@ -103,46 +160,63 @@ impl Subscription {
} }
} }
impl ReqFilter { fn prefix_match(prefixes: &[String], target: &str) -> bool {
/// Check for a match within the authors list. for prefix in prefixes {
// TODO: Ambiguity; what if the array is empty? Should we if target.starts_with(prefix) {
// consider that the same as null? return true;
fn authors_match(&self, event: &Event) -> bool { }
self.authors
.as_ref()
.map(|vs| vs.contains(&event.pubkey.to_owned()))
.unwrap_or(true)
} }
/// Check if this filter either matches, or does not care about the event tags. // none matched
fn event_match(&self, event: &Event) -> bool { false
self.event }
impl ReqFilter {
fn ids_match(&self, event: &Event) -> bool {
self.ids
.as_ref() .as_ref()
.map(|t| event.event_tag_match(t)) .map(|vs| prefix_match(vs, &event.id))
.unwrap_or(true) .unwrap_or(true)
} }
/// Check if this filter either matches, or does not care about fn authors_match(&self, event: &Event) -> bool {
/// the pubkey/petname tags. self.authors
fn pubkey_match(&self, event: &Event) -> bool {
self.pubkey
.as_ref() .as_ref()
.map(|t| event.pubkey_tag_match(t)) .map(|vs| prefix_match(vs, &event.pubkey))
.unwrap_or(true) .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. /// Check if this filter either matches, or does not care about the kind.
fn kind_match(&self, kind: u64) -> bool { 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. /// Determine if all populated fields in this filter match the provided event.
pub fn interested_in_event(&self, event: &Event) -> bool { 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.since.map(|t| event.created_at > t).unwrap_or(true)
&& self.kind_match(event.kind) && self.kind_match(event.kind)
&& self.authors_match(event) && self.authors_match(event)
&& self.pubkey_match(event) && self.tag_match(event)
&& self.event_match(event)
} }
} }
@@ -173,27 +247,66 @@ mod tests {
} }
#[test] #[test]
fn invalid_filter() { fn legacy_filter() {
// unrecognized field in filter // legacy field in filter
let raw_json = "[\"REQ\",\"some-id\",{\"foo\": 3}]"; let raw_json = "[\"REQ\",\"some-id\",{\"kind\": 3}]";
assert!(serde_json::from_str::<Subscription>(raw_json).is_err()); assert!(serde_json::from_str::<Subscription>(raw_json).is_ok());
} }
#[test] #[test]
fn author_filter() -> Result<()> { 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)?; let s: Subscription = serde_json::from_str(raw_json)?;
assert_eq!(s.id, "some-id"); assert_eq!(s.id, "some-id");
assert_eq!(s.filters.len(), 1); assert_eq!(s.filters.len(), 1);
let first_filter = s.filters.get(0).unwrap(); 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(()) Ok(())
} }
#[test] #[test]
fn interest_id_nomatch() -> Result<()> { fn interest_id_nomatch() -> Result<()> {
// subscription with a filter for ID // 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 { let e = Event {
id: "abcde".to_owned(), id: "abcde".to_owned(),
pubkey: "".to_owned(), pubkey: "".to_owned(),
@@ -202,15 +315,17 @@ mod tests {
tags: Vec::new(), tags: Vec::new(),
content: "".to_owned(), content: "".to_owned(),
sig: "".to_owned(), sig: "".to_owned(),
tagidx: None,
}; };
assert_eq!(s.interested_in_event(&e), false); assert!(!s.interested_in_event(&e));
Ok(()) Ok(())
} }
#[test] #[test]
fn interest_time_and_id() -> Result<()> { fn interest_time_and_id() -> Result<()> {
// subscription with a filter for ID and time // 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 { let e = Event {
id: "abc".to_owned(), id: "abc".to_owned(),
pubkey: "".to_owned(), pubkey: "".to_owned(),
@@ -219,8 +334,9 @@ mod tests {
tags: Vec::new(), tags: Vec::new(),
content: "".to_owned(), content: "".to_owned(),
sig: "".to_owned(), sig: "".to_owned(),
tagidx: None,
}; };
assert_eq!(s.interested_in_event(&e), false); assert!(!s.interested_in_event(&e));
Ok(()) Ok(())
} }
@@ -236,8 +352,9 @@ mod tests {
tags: Vec::new(), tags: Vec::new(),
content: "".to_owned(), content: "".to_owned(),
sig: "".to_owned(), sig: "".to_owned(),
tagidx: None,
}; };
assert_eq!(s.interested_in_event(&e), true); assert!(s.interested_in_event(&e));
Ok(()) Ok(())
} }
@@ -253,8 +370,9 @@ mod tests {
tags: Vec::new(), tags: Vec::new(),
content: "".to_owned(), content: "".to_owned(),
sig: "".to_owned(), sig: "".to_owned(),
tagidx: None,
}; };
assert_eq!(s.interested_in_event(&e), true); assert!(s.interested_in_event(&e));
Ok(()) Ok(())
} }
@@ -270,8 +388,9 @@ mod tests {
tags: Vec::new(), tags: Vec::new(),
content: "".to_owned(), content: "".to_owned(),
sig: "".to_owned(), sig: "".to_owned(),
tagidx: None,
}; };
assert_eq!(s.interested_in_event(&e), true); assert!(s.interested_in_event(&e));
Ok(()) Ok(())
} }
#[test] #[test]
@@ -287,8 +406,9 @@ mod tests {
tags: Vec::new(), tags: Vec::new(),
content: "".to_owned(), content: "".to_owned(),
sig: "".to_owned(), sig: "".to_owned(),
tagidx: None,
}; };
assert_eq!(s.interested_in_event(&e), true); assert!(s.interested_in_event(&e));
Ok(()) Ok(())
} }
@@ -304,8 +424,9 @@ mod tests {
tags: Vec::new(), tags: Vec::new(),
content: "".to_owned(), content: "".to_owned(),
sig: "".to_owned(), sig: "".to_owned(),
tagidx: None,
}; };
assert_eq!(s.interested_in_event(&e), false); assert!(!s.interested_in_event(&e));
Ok(()) Ok(())
} }
} }
+315
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());
}
}