Compare commits

..
Author SHA1 Message Date
Greg Heartsfield 0f276a06b2 Use notice function for sending messages 2021-11-26 10:32:14 -06:00
Greg Heartsfield 010c96b05f Moved nostr protocol handling to proto module 2021-11-26 10:17:20 -06:00
Greg Heartsfield 31057b6fd3 Rename license file 2021-11-25 21:32:53 -06:00
Greg Heartsfield 2238743c59 Don't bother breaking stream apart into read/write handles. 2021-11-25 21:00:28 -06:00
Greg Heartsfield 60319999a6 NOTICE sent for parsing errors 2021-11-25 20:31:08 -06:00
Greg Heartsfield 96f46866f7 README updates 2021-11-24 22:44:18 -06:00
Greg Heartsfield c758495de9 Querying for author/authors 2021-11-24 22:37:09 -06:00
Greg Heartsfield 580342f3a5 ReqFilter tests 2021-11-24 22:24:30 -06:00
Greg Heartsfield 89d2bcd598 Filter on additional fields (complete) 2021-11-24 22:16:11 -06:00
Greg Heartsfield 753958659d Check event tag filter 2021-11-24 22:10:42 -06:00
Greg Heartsfield c075194c3d Check for filter applicability with author(s) and kinds 2021-11-24 22:05:02 -06:00
Greg Heartsfield 296857d372 Start work on filtering events with subscriptions 2021-11-24 17:26:01 -06:00
Greg Heartsfield 03bc1fab18 API for checking if subscription matches key 2021-11-24 16:22:49 -06:00
Greg Heartsfield 5bc36cc591 Correctly replace subscriptions if ID matches 2021-11-24 16:22:28 -06:00
Greg Heartsfield 1a0f66a518 Perform subscribe/unsubscribe from websocket 2021-11-24 16:07:35 -06:00
Greg Heartsfield 2da46501ca Note/link for message propagation 2021-11-24 15:44:18 -06:00
Greg Heartsfield 6411aab7f9 Track subscription add/removes 2021-11-24 15:40:39 -06:00
Greg Heartsfield 03a46e20b0 track subscriptions in proto 2021-11-24 15:28:15 -06:00
Greg Heartsfield 433bb0f130 Rename request container enums 2021-11-24 15:22:31 -06:00
Greg Heartsfield 439174417a Correct length check for Close messages 2021-11-24 15:15:56 -06:00
Greg Heartsfield ecf2cf3094 Parse CLOSE requests 2021-11-24 14:59:45 -06:00
Greg Heartsfield a3a83722b2 Specify rust edition in rustfmt config 2021-11-24 14:44:07 -06:00
Greg Heartsfield 0bac2cbfc8 Rename request mod to subscription 2021-11-24 14:29:31 -06:00
Greg Heartsfield 0b7742cfb1 Simplify request message parsing 2021-11-24 14:24:35 -06:00
Greg Heartsfield 4e2ec93584 Successful parsing of requests from stream 2021-11-24 14:19:50 -06:00
Greg Heartsfield c001726402 Add test for request parsing 2021-11-24 13:48:40 -06:00
Greg Heartsfield 5e9fca1aeb Request parsing generally works 2021-11-24 13:33:38 -06:00
Greg Heartsfield bba66a1bce Request parsing 2021-11-24 12:25:55 -06:00
Greg Heartsfield 68a69505ac Start of protocol module 2021-11-24 12:25:38 -06:00
Greg Heartsfield 010a420465 Downgrade to 2018 edition (support rust-analyzer) 2021-11-24 09:59:00 -06:00
Greg Heartsfield bd7d083107 Work on request filter deserialization 2021-11-23 14:15:47 -06:00
Greg Heartsfield ba19c978a5 More error handling/debugging 2021-11-23 12:47:02 -06:00
Greg Heartsfield 25ac86cc68 More error handling/debugging 2021-11-23 12:35:25 -06:00
Greg Heartsfield 854531112d Wrap serde json errors 2021-11-23 11:32:19 -06:00
Greg Heartsfield 5224b9159e Structure for parsing types 2021-11-23 10:46:47 -06:00
Greg Heartsfield ae68fdd896 Error and basic protocol handling 2021-11-23 10:29:53 -06:00
Greg Heartsfield 5e75370a67 Properly deserialize tags if null 2021-11-21 22:01:34 -06:00
Greg Heartsfield d81949c31b Modules to hold error def and request/query def 2021-11-21 21:58:16 -06:00
Greg Heartsfield 116feace87 Add some (broken) deserializers and tests for json events 2021-11-21 21:57:44 -06:00
Greg Heartsfield 2d0efd8db4 Add some dependencies for dealing with crypto/bytes 2021-11-21 21:57:11 -06:00
Greg Heartsfield c2704af0d6 Simple event struct with serialization 2021-11-21 18:14:38 -06:00
Greg Heartsfield 0fe2423b22 Add license and readme 2021-11-21 17:35:58 -06:00
Greg Heartsfield b2ca2f6743 Begin modules for nostr protocol 2021-11-21 17:22:42 -06:00
Greg Heartsfield cf6171aadc Handle client messages that exceed max size 2021-11-21 16:03:03 -06:00
Greg Heartsfield dbb535eaaa Separate server and connection handling functions 2021-11-21 13:48:31 -06:00
Greg Heartsfield ee27d0c597 Warning cleanup 2021-11-21 13:32:15 -06:00
Greg Heartsfield 259c115d67 Configure tokio runtime manually 2021-11-21 13:31:23 -06:00
Greg Heartsfield efa14418fc Sample messages from a public relay 2021-11-21 09:03:34 -06:00
Greg Heartsfield 097199ae6a Do not panic on normal errors 2021-11-20 21:33:35 -06:00
Greg Heartsfield f9e30c194f Demo server (compiles) 2021-11-20 20:59:40 -06:00
Greg Heartsfield 738e651574 Ignore rust build area 2021-11-20 20:59:23 -06:00
Greg Heartsfield 506b92f505 Add dependency on tokio-tungstenite for websockets 2021-11-20 20:44:32 -06:00
Greg Heartsfield cd1bc7655c Cargo init 2021-11-20 20:28:10 -06:00
24 changed files with 785 additions and 5192 deletions
-1
View File
@@ -1,2 +1 @@
/target
nostr.db
Generated
+167 -1191
View File
File diff suppressed because it is too large Load Diff
+21 -28
View File
@@ -1,32 +1,25 @@
[package]
name = "nostr-rs-relay"
version = "0.6.0"
edition = "2021"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "^0.4"
env_logger = "^0.9"
tokio = { version = "^1.16", features = ["full"] }
futures = "^0.3"
futures-util = "^0.3"
tokio-tungstenite = "^0.17"
tungstenite = "^0.17"
thiserror = "^1"
uuid = { version = "^0.8", features = ["v4"] }
config = { version = "^0.12", features = ["toml"] }
bitcoin_hashes = { version = "^0.10", features = ["serde"] }
secp256k1 = {version = "^0.21", features = ["rand", "rand-std", "serde", "bitcoin_hashes"] }
serde = { version = "^1.0", features = ["derive"] }
serde_json = {version = "^1.0", features = ["preserve_order"]}
hex = "^0.4"
rusqlite = { version = "^0.26", features = ["limits"]}
r2d2 = "^0.8"
r2d2_sqlite = "^0.19"
lazy_static = "^1.4"
governor = "^0.4"
nonzero_ext = "^0.3"
hyper = { version="0.14", features=["client", "server","http1","http2","tcp"] }
hyper-tls = "^0.5"
http = { version = "^0.2" }
parse_duration = "^2"
rand = "^0.8"
log = "0.4.14"
env_logger = "0.9.0"
tokio = { version = "1.14.0", features = ["full"] }
futures = "0.3.18"
futures-util = "0.3.18"
tokio-tungstenite = "0.16.0"
tungstenite = "0.16.0"
thiserror = "1.0.30"
uuid = { version = "0.8", features = ["v4"] }
bitcoin_hashes = { version = "0.10.0", features = ["serde"] }
secp256k1 = { version = "0.20.3", features = ["rand", "rand-std", "serde", "bitcoin_hashes"] }
serde = { version = "1.0.130", features = ["derive"] }
serde_json = "1.0.71"
serde_repr = "0.1.7"
base64 = "0.13.0"
-42
View File
@@ -1,42 +0,0 @@
FROM rust:1.59.0 as builder
RUN USER=root cargo new --bin nostr-rs-relay
WORKDIR ./nostr-rs-relay
COPY ./Cargo.toml ./Cargo.toml
COPY ./Cargo.lock ./Cargo.lock
RUN cargo build --release
RUN rm src/*.rs
COPY ./src ./src
RUN rm ./target/release/deps/nostr*relay*
RUN cargo build --release
FROM debian:bullseye-20220125-slim
ARG APP=/usr/src/app
ARG APP_DATA=/usr/src/app/db
RUN apt-get update \
&& apt-get install -y ca-certificates tzdata sqlite3 libc6 \
&& rm -rf /var/lib/apt/lists/*
EXPOSE 8080
ENV TZ=Etc/UTC \
APP_USER=appuser
RUN groupadd $APP_USER \
&& useradd -g $APP_USER $APP_USER \
&& mkdir -p ${APP} \
&& mkdir -p ${APP_DATA}
COPY --from=builder /nostr-rs-relay/target/release/nostr-rs-relay ${APP}/nostr-rs-relay
RUN chown -R $APP_USER:$APP_USER ${APP}
USER $APP_USER
WORKDIR ${APP}
ENV RUST_LOG=info
ENV APP_DATA=${APP_DATA}
CMD ./nostr-rs-relay --db ${APP_DATA}
+4 -79
View File
@@ -1,89 +1,14 @@
# [nostr-rs-relay](https://git.sr.ht/~gheartsfield/nostr-rs-relay)
[nostr-rs-relay](https://git.sr.ht/~gheartsfield/nostr-rs-relay)
===
This is a [nostr](https://github.com/fiatjaf/nostr) relay, written in
Rust. It currently supports the entire relay protocol, and has a
SQLite persistence layer.
Rust. Very much a work-in-progress, and a hobby project to learn
Rust, and tokio/async.
The project master repository is available on
[sourcehut](https://sr.ht/~gheartsfield/nostr-rs-relay/), and is
mirrored on [GitHub](https://github.com/scsibug/nostr-rs-relay).
## Features
NIPs with a relay-specific implementation are listed here.
- [x] NIP-01: Core event model
- [x] NIP-01: Hide old metadata events
- [x] NIP-01: Id/Author prefix search (_experimental_)
- [x] NIP-02: Hide old contact list events
- [ ] NIP-03: OpenTimestamps
- [x] NIP-05: Mapping Nostr keys to DNS identifiers
- [x] NIP-09: Event deletion
- [x] NIP-11: Relay information document
- [x] NIP-12: Generic tag search (_experimental_)
## Quick Start
The provided `Dockerfile` will compile and build the server
application. Use a bind mount to store the SQLite database outside of
the container image, and map the container's 8080 port to a host port
(7000 in the example below).
```console
$ docker build -t nostr-rs-relay .
$ docker run -it -p 7000:8080 \
--mount src=$(pwd)/data,target=/usr/src/app/db,type=bind nostr-rs-relay
[2021-12-31T19:58:31Z INFO nostr_rs_relay] listening on: 0.0.0.0:8080
[2021-12-31T19:58:31Z INFO nostr_rs_relay::db] opened database "/usr/src/app/db/nostr.db" for writing
[2021-12-31T19:58:31Z INFO nostr_rs_relay::db] DB version = 2
```
Use a `nostr` client such as
[`noscl`](https://github.com/fiatjaf/noscl) to publish and query
events.
```console
$ noscl publish "hello world"
Sent to 'ws://localhost:8090'.
Seen it on 'ws://localhost:8090'.
$ noscl home
Text Note [81cf...2652] from 296a...9b92 5 seconds ago
hello world
```
A pre-built container is also available on DockerHub:
https://hub.docker.com/r/scsibug/nostr-rs-relay
## Configuration
The sample [`config.toml`](config.toml) file demonstrates the
configuration available to the relay. This file is optional, but may
be mounted into a docker container like so:
```console
$ docker run -it -p 7000:8080 \
--mount src=$(pwd)/config.toml,target=/usr/src/app/config.toml,type=bind \
--mount src=$(pwd)/data,target=/usr/src/app/db,type=bind \
nostr-rs-relay
```
Options include rate-limiting, event size limits, and network address
settings.
## Reverse Proxy Configuration
For examples of putting the relay behind a reverse proxy (for TLS
termination, load balancing, and other features), see [Reverse
Proxy](reverse-proxy.md).
## Dev Channel
For development discussions, please feel free to use the [sourcehut
mailing list](https://lists.sr.ht/~gheartsfield/nostr-rs-relay-devel).
Or, drop by the [Nostr Telegram Channel](https://t.me/nostr_protocol).
License
---
This project is MIT licensed.
-102
View File
@@ -1,102 +0,0 @@
# Nostr-rs-relay configuration
[info]
# The advertised URL for the Nostr websocket.
relay_url = "wss://nostr.example.com/"
# Relay information for clients. Put your unique server name here.
name = "nostr-rs-relay"
# Description
description = "A newly created nostr-rs-relay.\n\nCustomize this with your own info."
# Administrative contact pubkey
#pubkey = "0c2d168a4ae8ca58c9f1ab237b5df682599c6c7ab74307ea8b05684b60405d41"
# Administrative contact URI
#contact = "mailto:contact@example.com"
[database]
# Directory for SQLite files. Defaults to the current directory. Can
# also be specified (and overriden) with the "--db dirname" command
# line option.
data_directory = "."
# Database connection pool settings for subscribers:
# Minimum number of SQLite reader connections
#min_conn = 4
# Maximum number of SQLite reader connections
#max_conn = 128
[network]
# Bind to this network address
address = "0.0.0.0"
# Listen on this port
port = 8080
[options]
# Reject events that have timestamps greater than this many seconds in
# the future. Defaults to rejecting anything greater than 30 minutes
# from the current time.
reject_future_seconds = 1800
[limits]
# Limit events created per second, averaged over one minute. Must be
# an integer. If not set (or set to 0), defaults to unlimited.
#messages_per_sec = 0
# Limit the maximum size of an EVENT message. Defaults to 128 KB.
# Set to 0 for unlimited.
#max_event_bytes = 131072
# Maximum WebSocket message in bytes. Defaults to 128 KB.
#max_ws_message_bytes = 131072
# Maximum WebSocket frame size in bytes. Defaults to 128 KB.
#max_ws_frame_bytes = 131072
# Broadcast buffer size, in number of events. This prevents slow
# readers from consuming memory.
#broadcast_buffer = 16384
# Event persistence buffer size, in number of events. This provides
# backpressure to senders if writes are slow.
#event_persist_buffer = 4096
[authorization]
# Pubkey addresses in this array are whitelisted for event publishing.
# Only valid events by these authors will be accepted, if the variable
# is set.
#pubkey_whitelist = [
# "35d26e4690cbe1a898af61cc3515661eb5fa763b57bd0b42e45099c8b32fd50f",
# "887645fef0ce0c3c1218d2f5d8e6132a19304cdc57cd20281d082f38cfea0072",
#]
[verified_users]
# NIP-05 verification of users. Can be "enabled" to require NIP-05
# metadata for event authors, "passive" to perform validation but
# never block publishing, or "disabled" to do nothing.
#mode = "disabled"
# Domain names that will be prevented from publishing events.
#domain_blacklist = ["wellorder.net"]
# Domain names that are allowed to publish events. If defined, only
# events NIP-05 verified authors at these domains are persisted.
#domain_whitelist = ["example.com"]
# Consider an pubkey "verified" if we have a successful validation
# from the NIP-05 domain within this amount of time. Note, if the
# domain provides a successful response that omits the account,
# verification is immediately revoked.
#verify_expiration = "1 week"
# How long to wait between verification attempts for a specific author.
#verify_update_frequency = "24 hours"
# How many consecutive failed checks before we give up on verifying
# this author.
#max_consecutive_failures = 20
-248
View File
@@ -1,248 +0,0 @@
# Author Verification Design Document
The relay will use NIP-05 DNS-based author verification to limit which
authors can publish events to a relay. This document describes how
this feature will operate.
## Considerations
DNS-based author verification is designed to be deployed in relays that
want to prevent spam, so there should be strong protections to prevent
unauthorized authors from persisting data. This includes data needed to
verify new authors.
There should be protections in place to ensure the relay cannot be
used to spam or flood other webservers. Additionally, there should be
protections against server-side request forgery (SSRF).
## Design Overview
### Concepts
All authors are initially "unverified". Unverified authors that submit
appropriate `NIP-05` metadata events become "candidates" for
verification. A candidate author becomes verified when the relay
inspects a kind `0` metadata event for the author with a `nip05` field,
and follows the procedure in `NIP-05` to successfully associate the
author with an internet identifier.
The `NIP-05` procedure verifies an author for a fixed period of time,
configurable by the relay operator. If this "verification expiration
time" (`verify_expiration`) is exceeded without being refreshed, they
are once again unverified.
Verified authors have their status regularly and automatically updated
through scheduled polling to their verified domain, this process is
"re-verification". It is performed based on the configuration setting
`verify_update_frequency`, which defines how long the relay waits
between verification attempts (whether the result was success or
failure).
Authors may change their verification data (the internet identifier from
`NIP-05`) with a new metadata event, which then requires
re-verification. Their old verification remains valid until
expiration.
Performing candidate author verification is a best-effort activity and
may be significantly rate-limited to prevent relays being used to
attack other hosts. Candidate verification (untrusted authors) should
never impact re-verification (trusted authors).
## Operating Modes
The relay may operate in one of three modes. "Disabled" performs no
validation activities, and will never permit or deny events based on
an author's NIP-05 metadata. "Passive" performs NIP-05 validation,
but does not permit or deny events based on the validity or presence
of NIP-05 metadata. "Enabled" will require current and valid NIP-05
metadata for any events to be persisted. "Enabled" mode will
additionally consider domain whitelist/blacklist configuration data to
restrict which author's events are persisted.
## Design Details
### Data Storage
Verification is stored in a dedicated table. This tracks:
* `nip05` identifier
* most recent verification timestamp
* most recent verification failure timestamp
* reference to the metadata event (used for tracking `created_at` and
`pubkey`)
### Event Handling
All events are first validated to ensure the signature is valid.
Incoming events of kind _other_ than metadata (kind `0`) submitted by
clients will be evaluated as follows.
* If the event's author has a current verification, the event is
persisted as normal.
* If the event's author has either no verification, or the
verification is expired, the event is rejected.
If the event is a metadata event, we handle it differently.
We first determine the verification status of the event's pubkey.
* If the event author is unverified, AND the event contains a `nip05`
key, we consider this a verification candidate.
* If the event author is unverified, AND the event does not contain a
`nip05` key, this is not a candidate, and the event is dropped.
* If the event author is verified, AND the event contains a `nip05`
key that is identical to the currently stored value, no special
action is needed.
* If the event author is verified, AND the event contains a different
`nip05` than was previously verified, with a more recent timestamp,
we need to re-verify.
* If the event author is verified, AND the event is missing a `nip05`
key, and the event timestamp is more recent than what was verified,
we do nothing. The current verification will be allowed to expire.
### Candidate Verification
When a candidate verification is requested, a rate limit will be
utilized. If the rate limit is exceeded, new candidate verification
requests will be dropped. In practice, this is implemented by a
size-limited channel that drops events that exceed a threshold.
Candidates are never persisted in the database.
### Re-Verification
Re-verification is straightforward when there has been no change to
the `nip05` key. A new request to the `nip05` domain is performed,
and if successful, the verification timestamp is updated to the
current time. If the request fails due to a timeout or server error,
the failure timestamp is updated instead.
When the the `nip05` key has changed and this event is more recent, we
will create a new verification record, and delete all other records
for the same name.
Regarding creating new records vs. updating: We never update the event
reference or `nip05` identifier in a verification record. Every update
either reset the last failure or last success timestamp.
### Determining Verification Status
In determining if an event is from a verified author, the following
procedure should be used:
Join the verification table with the event table, to provide
verification data alongside the event `created_at` and `pubkey`
metadata. Find the most recent verification record for the author,
based on the `created_at` time.
Reject the record if the success timestamp is not within our
configured expiration time.
Reject records with disallowed domains, based on any whitelists or
blacklists in effect.
If a result remains, the author is treated as verified.
This does give a time window for authors transitioning their verified
status between domains. There may be a period of time in which there
are multiple valid rows in the verification table for a given author.
### Cleaning Up Inactive Verifications
After a author verification has expired, we will continue to check for
it to become valid again. After a configurable number of attempts, we
should simply forget it, and reclaim the space.
### Addition of Domain Whitelist/Blacklist
A set of whitelisted or blacklisted domains may be provided. If both
are provided, only the whitelist is used. In this context, domains
are either "allowed" (present on a whitelist and NOT present on a
blacklist), or "denied" (NOT present on a whitelist and present on a
blacklist).
The processes outlined so far are modified in the presence of these
options:
* Only authors with allowed domains can become candidates for
verification.
* Verification status queries additionally filter out any denied
domains.
* Re-verification processes only proceed with allowed domains.
### Integration
We have an existing database writer thread, which receives events and
attempts to persist them to disk. Once validated and persisted, these
events are broadcast to all subscribers.
When verification is enabled, the writer must check to ensure a valid,
unexpired verification record exists for the auther. All metadata
events (regardless of verification status) are forwarded to a verifier
module. If the verifier determines a new verification record is
needed, it is also responsible for persisting and broadcasting the
event, just as the database writer would have done.
## Threat Scenarios
Some of these mitigations are fully implemented, others are documented
simply to demonstrate a mitigation is possible.
### Domain Spamming
*Threat*: A author with a high-volume of events creates a metadata event
with a bogus domain, causing the relay to generate significant
unwanted traffic to a target.
*Mitigation*: Rate limiting for all candidate verification will limit
external requests to a reasonable amount. Currently, this is a simple
delay that slows down the HTTP task.
### Denial of Service for Legitimate Authors
*Threat*: A author with a high-volume of events creates a metadata event
with a domain that is invalid for them, _but which is used by other
legitimate authors_. This triggers rate-limiting against the legitimate
domain, and blocks authors from updating their own metadata.
*Mitigation*: Rate limiting should only apply to candidates, so any
existing verified authors have priority for re-verification. New
authors will be affected, as we can not distinguish between the threat
and a legitimate author. _(Unimplemented)_
### Denial of Service by Consuming Storage
*Threat*: A author creates a high volume of random metadata events with
unique domains, in order to cause us to store large amounts of data
for to-be-verified authors.
*Mitigation*: No data is stored for candidate authors. This makes it
harder for new authors to become verified, but is effective at
preventing this attack.
### Metadata Replay for Verified Author
*Threat*: Attacker replays out-of-date metadata event for a author, to
cause a verification to fail.
*Mitigation*: New metadata events have their signed timestamp compared
against the signed timestamp of the event that has most recently
verified them. If the metadata event is older, it is discarded.
### Server-Side Request Forgery via Metadata
*Threat*: Attacker includes malicious data in the `nip05` event, which
is used to generate HTTP requests against potentially internal
resources. Either leaking data, or invoking webservices beyond their
own privileges.
*Mitigation*: Consider detecting and dropping when the `nip05` field
is an IP address. Allow the relay operator to utilize the `blacklist`
or `whitelist` to constrain hosts that will be contacted. Most
importantly, the verification process is hardcoded to only make
requests to a known url path
(`.well-known/nostr.json?name=<LOCAL_NAME>`). The `<LOCAL_NAME>`
component is restricted to a basic ASCII subset (preventing additional
URL components).
-53
View File
@@ -1,53 +0,0 @@
# 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.
+99
View File
@@ -0,0 +1,99 @@
["EVENT","5496183021909538",{"id":"c352327610efbfe08ab49d8452f7827f0d4798d3c28c1143ef5a4c8d531fa1fa","pubkey":"d843184b145ec26c673b77b9eac5321af19195cb48470165d20e56fe7e727cb7","created_at":1612650401,"kind":1,"tags":null,"content":"gdfshbsdfgb","sig":"ebd534a85fc8510dbcf92023ff9012b89e7408b52d347af788fa223f5df83eae6ab5fced5ab2e3c265bfda9cd6f428b0cb4cdcbd1ac5a13a5cc3265df5d7942d"}]
["EVENT","5496183021909538",{"id":"1384757da583e6129ce831c3d7afc775a33a090578f888dd0d010328ad047d0c","pubkey":"bbbd9711d357df4f4e498841fd796535c95c8e751fa35355008a911c41265fca","created_at":1612650459,"kind":1,"tags":null,"content":"hello world","sig":"59d0cc47ab566e81f72fe5f430bcfb9b3c688cb0093d1e6daa49201c00d28ecc3651468b7938642869ed98c0f1b262998e49a05a6ed056c0d92b193f4e93bc21"}]
["EVENT","5496183021909538",{"id":"e7ade13f425e3c71a7ebd9d5176059f6ce0469f06317e804cb57c35b71e791da","pubkey":"40334685ed272a062c1a3bac85c995c53c69cbfbd0a02f1a134679986fd39af1","created_at":1612651366,"kind":1,"tags":null,"content":"Hello world","sig":"cefb82b57c47e8ed6fda7bf06513d653b2081cc25817f55c8e56005c84390aedc7a7b8464fde46afad890d1d355c63da2c9c4d8b64cccc70df2232f9f4cf86bb"}]
["EVENT","5496183021909538",{"id":"dfcdc27e775dc27024689fcdab44cbf7ec5660bcded5beb25f079aa05468fcc9","pubkey":"09488d7a12468dda7839520515c20e6c2a8272c5df8ec0261db2ec65d4420a39","created_at":1612654851,"kind":4,"tags":null,"content":"HOfkGd7fA2vIsbXuZazDrA==?iv=tAfeHJFgxIjuUvxqlxCcLg==","sig":"7176125524d751e810f8523360eb319076e46d97e70928a0bcf3b6bec9efd121f01adbcf196d428a1a7691f5c94df3c4c3478c9dbd3113745bdb96cc2c8a0a49"}]
["EVENT","5496183021909538",{"id":"d349109f225a0e1685913a2559a6a9beca0ec0d6865d617e237a320800dd8fc3","pubkey":"09488d7a12468dda7839520515c20e6c2a8272c5df8ec0261db2ec65d4420a39","created_at":1612654966,"kind":4,"tags":null,"content":"esxa2yzo8b/3xR574joX4g==?iv=2LinF8Zd1fs3DCAmt4cLZg==","sig":"927a81962830e6f2d437e1f5d5cf123e0b536ad5492469483373a02e7f3c1ef4367b121a8e58aed062b07c5f634b108a2d67965e24086337b63e395888c9de9b"}]
["EVENT","5496183021909538",{"id":"2740f9572b8330565103640e94c608c561150d115f38d1c9230b221222c7ffec","pubkey":"5d6baec9a3dce295e6a2ae16fef2aa1849d2753d047a4abc16f53183d971183d","created_at":1612702779,"kind":1,"tags":null,"content":"Hello world","sig":"37dd4a4beccabb01a9ae4661953807c82d70d92e92495a5647b83670845063505eee8daf0a0a9f845e74f6420911e6a585fe39a94aefb7f2a274a30912178ae3"}]
["EVENT","5496183021909538",{"id":"a629291da57e02ecfa72ae4dfab772bb31720756fcd51a0247f1753fbf9afd30","pubkey":"7bb57b0f744317c7300dda43a3e64ebf5e5aff1b180619641de1767bad543ca3","created_at":1612706239,"kind":1,"tags":null,"content":"Hello world","sig":"ae540e301af3b4836d0c6e334076a26de7879cf3a31f28d2599e0c9d71e64f30ba12c3675368eaed324136338c6bb3aaf3f7e6fb62f3e041f6f7e5a6726a4234"}]
["EVENT","5496183021909538",{"id":"6ec3918617f694b5dd8bc0a3524a6b341e42425d45dd89784dbc79642cdbb6c0","pubkey":"f8e9ba8fb6f401c6f85ec7671f4b6879ff9ae4d867508d00cd42fcaa43e01974","created_at":1612706419,"kind":1,"tags":null,"content":"hello world","sig":"b8e49754c9dd7f6898a28ed2b5d002026a559f119171a22db7095b0c2fee995475d06dda11a9f482b3b6766c375a2b0c2204ba1fe0a58ef312a90db0a68f9def"}]
["EVENT","5496183021909538",{"id":"7a84cab2e6e7ced2ea2bf29d07a739a140c3afe2fefbe83a0578ac6cc7691f66","pubkey":"f56bd7d9e38ef70e5355dd1adcfa2d63b4f8041157b75085a61ad201022067f4","created_at":1612706661,"kind":1,"tags":null,"content":"Hello world","sig":"99fab70b7e51e4bf17eacd246235c06e369c2cfeae1afb76a47f5fc0d9f09ea73da7282a8c3ed0da4d38519159fecd2c06d942afa33f9a44eae5a98922d883ed"}]
["EVENT","5496183021909538",{"id":"f01d8c4d834a6c7bd21339f61f42bc282c39c4ec20b9165e197c70ef2ec45bb6","pubkey":"f8e9ba8fb6f401c6f85ec7671f4b6879ff9ae4d867508d00cd42fcaa43e01974","created_at":1612706788,"kind":1,"tags":null,"content":"😘Nostr mooon","sig":"795ce321c1b3f795cd89cd56a6e155d459d1acc49f7d99bfb0c5601be03a6ba6e19a78b7701b2161c0e005ba12ec956693e3b04ab5926b4420137f97044bb62d"}]
["EVENT","5496183021909538",{"id":"d424b06c3ab880b4456bb892d824a193c37fec5905127c47e0a58c01f20a8805","pubkey":"f8e9ba8fb6f401c6f85ec7671f4b6879ff9ae4d867508d00cd42fcaa43e01974","created_at":1612706836,"kind":4,"tags":null,"content":"ZDN1DW9WVKy9gtYk8guCXw==?iv=Wni55RkHsIiFr2LDKZGnvA==","sig":"d735906490710367234b0dbd0f656e38862f77b3df3a1b02be240d79c252e21be435c82f943f97af5814135655a052d49ba1a60b623231c71e5aa3e1f05576f3"}]
["EVENT","5496183021909538",{"id":"3bc86162407605dfcf629fb1568cdd7b8bef311e4a347230965e3701e0efb550","pubkey":"f56bd7d9e38ef70e5355dd1adcfa2d63b4f8041157b75085a61ad201022067f4","created_at":1612706876,"kind":4,"tags":null,"content":"5hRRuFOeVXk6eDBLtd90DQ==?iv=uvxudX52hqSgoNmLoCa7yw==","sig":"4ad25d91b31c022862d905a3d22cf539aeb0d407e514a495d93271beb87dc8b6d52584c9751e0f2282db3e9ef6d3b954b24fec68b99ccc473efe00eb79902a28"}]
["EVENT","5496183021909538",{"id":"03f2d548da33ba1d57f0799ecca65024c22921d401c5664105613084d556e1ca","pubkey":"f56bd7d9e38ef70e5355dd1adcfa2d63b4f8041157b75085a61ad201022067f4","created_at":1612706887,"kind":4,"tags":null,"content":"ijb0cRAp01jqCrsdf6x4XA==?iv=rqfq4NZOYH+aP33PX2u/7g==","sig":"08085386e584a6c35110e32def29325ab82012d3a37074d8144976448d1b15e869af632cf505a2ebe4c2ec944ffc232760f7adb1b9c535122ab3b08e64dff548"}]
["EVENT","5496183021909538",{"id":"94010c0e91f5cc089eb92dc13d52a5230ec3b2b853139f8a5e4cb22b329824a3","pubkey":"f8e9ba8fb6f401c6f85ec7671f4b6879ff9ae4d867508d00cd42fcaa43e01974","created_at":1612706924,"kind":4,"tags":null,"content":"LARSxV4ftsCYjdZ6AS3x8g==?iv=mhXHc8fblD1SghCpH9pcYQ==","sig":"f5e4c5f4cdfe0f2f42fbb843a254d225b718a1e3461030b721f6fa3b79cdeab27e4066e0736a8b172af7f4ceced66b62d183f215aa8cd05bd07664cc8c833f47"}]
["EVENT","5496183021909538",{"id":"6cc318ed826f77fce5ce0b2c64621693cb6d89ae8671703d3bcecbb49fb2d46e","pubkey":"545423ec685844531c29a2947793f9eb030bb33857651f410e7eee09d7af07d8","created_at":1612707587,"kind":1,"tags":null,"content":"Hello world","sig":"7e0bb2a6da5a5964a933e893767240bdc7d40d419296743775a6bf574c558063884c86912cb5ba50f7037bd12e2b6764dfcaa61c1dd0156f017897311fc4ddd5"}]
["EVENT","5496183021909538",{"id":"c2b2675d252f5660d5551bc74d619f9f434388d2d170ee64c8e16ae5e802b663","pubkey":"d6cd8c07574eb86ffe5e3d870f7dd5f77f2284e16dfed0ebe1417264d70bd2bc","created_at":1612712970,"kind":1,"tags":null,"content":"poo","sig":"e1dc366fc7f99cbc976f91382a2165bc0e481570a8d04c2745f82aabc2465c1f9bafaccfd8ff20ae250e5abfcec6dd86a2df5c5d2063d089e7e1100d12e10fc7"}]
["EVENT","5496183021909538",{"id":"34ec7e92578415fa9b870ab3dbc914f8cd1a69728d92455ef281719a8ba98d45","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612713213,"kind":1,"tags":null,"content":"Hello world!","sig":"facbe66cac0e1761e48ce5e9874a39013d899e359e82822e91aa8b955febcdd555b2fa201f343bea198912a91eb8cd8a1f05ce6bba7c1fd4b070881eada76403"}]
["EVENT","5496183021909538",{"id":"e1dacb8a6ca63fc8a144e3d5e5f18892e21cf6b8dfd7eb019d0443cfceba4bab","pubkey":"2b28d10527292d478c4653402b085d831a3b78759e46f7b46de3b7640e2ad667","created_at":1612713276,"kind":1,"tags":null,"content":"Yo","sig":"b1dd07fb7e97b4822de7f2854a603cc04f4e081427193d8495e8628f7994f0dbbadc367680bb5a2fa0f56699e01862fbf8e452f0698bbf66536d732663d32876"}]
["EVENT","5496183021909538",{"id":"1f8cd2db4c7e7153f5b7b9e67953980ace79af78e47bba6b2a5e66ab93c749bb","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612713555,"kind":1,"tags":null,"content":"Follow your mates!","sig":"1727f52b4257f1f6fba8a6fe0b6b0180ee404f76fde18427840fa4d513010ba802b4b8fd261209ab196465b3ffe11e8d7ccbbc1aa557d36a4be753b251eb03d1"}]
["EVENT","5496183021909538",{"id":"a94596ffc55baa20157dac4c09544fe2e925d9cc21cb5765a6744762892bbeab","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612713686,"kind":4,"tags":null,"content":"sidnxTMcUmJWv5esjKgdbQ==?iv=k885PbcWY/1Rs4SOVuVBpw==","sig":"0653438b09f25101bd46a118752adda07fa8eebc89b1aab725ca6c4e09d028d9b6d28e16382b4d0bfb583cbcb142fbe8ff59815f898168b287bc288128eda5a7"}]
["EVENT","5496183021909538",{"id":"cbe6c74e1a8337ad1c90a4d2cf4602942070735a35330c6603b92cf406e61a93","pubkey":"2b28d10527292d478c4653402b085d831a3b78759e46f7b46de3b7640e2ad667","created_at":1612713689,"kind":4,"tags":null,"content":"GnBL88R3Q8UyM14hqJYM9w==?iv=uqJo8XCW1kFnK6EV/gZAKg==","sig":"95f6273f6be520af9f84ea1249e57b755f6f4b773708abc5c9aed2f46817e92e04012c34e5d9a0f5604774df4bcb1dec2f32dbc7599fb051fd774352c209b9a1"}]
["EVENT","5496183021909538",{"id":"90c8b94d5687efa38bf7ab27d6f6d334ad66a6a6ce8a2a85c1ce1a1fcb624938","pubkey":"2b28d10527292d478c4653402b085d831a3b78759e46f7b46de3b7640e2ad667","created_at":1612713727,"kind":4,"tags":null,"content":"9fFEbgB4zpC+w2ebOU+75Q==?iv=Sr0fAFqjt2bKSrg67ExELQ==","sig":"03a224e416b1cdc8c2690905e7dfe7e44c3470e9fbe49e63717d603717900d4c5fb20c129433e8308cd170ea901a10e29833d33df7c8eb00f67da6eec4b24ee1"}]
["EVENT","5496183021909538",{"id":"c3f5afc494bba4f3859fba2d89af388b006eaacdcea2085778a852d84dd91d69","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612714060,"kind":4,"tags":null,"content":"PGRihYEwIGDBepMSwSIzAw==?iv=qsiS+2Tpuvm/XjHzfeSZsA==","sig":"55dc0046403e3369fe1dbd05053ccb74d130ad13bbd49fa07a228bbcb0c2a2168b3eae2176179a7ac5470a6af5b2eb3fd22bc2e4f4f5583f26b9affd6175c7c9"}]
["EVENT","5496183021909538",{"id":"7d55fca77dc47d50e4b2a36ebf178de9e4e9108e51324ee5ebef101faeddf295","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612717927,"kind":1,"tags":null,"content":"Hello, Dolu there","sig":"df6ef0dcd4f698436102b65c2712e80b8eb8e0af57ddec958e7dc123b7861efc3e52c81ff29a9bd3491e97b43168f5babbed1da3fd552e04e2df50c62f24e987"}]
["EVENT","5496183021909538",{"id":"49e822a04f7172c7cae32c1d02191eee78bb741a743c79988a4bc7c972ae1ee1","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612717967,"kind":4,"tags":null,"content":"lZjRPAfWaP4zHB8SRS93KA==?iv=KSj6xm+7L5QcKazYem+OJw==","sig":"58192c278352bf560fd0875a329c64f9270ebe193246ffc3506782e37c52aed449b073f420e1759bc72c536ce5e01a2b948bfdf9cec46b7ebbce393ac4df1fbd"}]
["EVENT","5496183021909538",{"id":"dc6ce72faea7c32260726d0d8046cd1365f25ca31d9889cde2fd7a6ecca22865","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612718029,"kind":4,"tags":null,"content":"PR0zmMZVkfdzsJriO3ajdQ==?iv=bML7a18IESplBVx2Fo5gsQ==","sig":"23efe1d673d3d6b10dc1dc21f573a1ae925917435a68622121b12bd539301c0ab112c555bab675a71a917cf155d5c39f90fcbcec7a2b74697114e88733376ce0"}]
["EVENT","5496183021909538",{"id":"beb313771c25ba6a62f40f24eed6eaf7fea82a7fac8cfbbdda78f3b4c7ef208f","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612718038,"kind":4,"tags":null,"content":"8+83lQr6CjtxUV8jmh/sVw==?iv=TBpFUY7D/sNPSbTzToMn6g==","sig":"47c0dcdd9c432fa83f3153cad76359cc74cdceefe7f9c66db0732091f8367b57c6cdec7f1de0d0f395099da9ce2c52069396ad80ff8fab302689ee13217a927e"}]
["EVENT","5496183021909538",{"id":"b6efa79297151c0f25948ed586c5fb23c04453043cf24d961e8d0eb8b8c83707","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612718072,"kind":1,"tags":null,"content":"Not getting any posts yet?","sig":"70c18033c32b3f95636b15d6013d18360bb734f5bcbb1c8bf5993d8f890e75e4158531a0ab9a48d49bcbc1b94f94163d6c80d9b89e6bf23edbda9d14fcb16d6a"}]
["EVENT","5496183021909538",{"id":"8a0f6f93dd51c279527e891dfbd57ab95d02d9fc7d331418d75c7ea79158a0fb","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612718144,"kind":1,"tags":null,"content":"Hellooooo","sig":"eb2fff856a92b2518bcc90fc965d527a57ad91a9ce6dabf5c96d35d6c7440036ed9523157521ad7cfc3327a540c392c4a751008cc2ebf3b433d81f535c45ba71"}]
["EVENT","5496183021909538",{"id":"f450d18411b503b30d5a8cebaeb0480ba6c8eb67fda572d04c63fb900d95297a","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612718156,"kind":1,"tags":null,"content":"no","sig":"9a0c6663fdb527f8dbd1668acda6459814c738918cece209f7fb65f973cc3fb32ee7fb61e5f9e461f443d6f775a795498bcc19db2127803a8588044f07bdca0a"}]
["EVENT","5496183021909538",{"id":"9d73afa3e3ea6b4e930504bb34c8066d655d1878fdc1cff39ed9b093fc3324ab","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612718170,"kind":1,"tags":null,"content":"I'm fiatjaf, and I can't prove!","sig":"89d217bfc23463fb326507765a35661ee64b2e474656bacf3511423f2784a47c1a163ccc6bcc054440b40310bbd280e712fb6cdeb23c1d486908b5e8b71e55fe"}]
["EVENT","5496183021909538",{"id":"c9ae8727e529f4e6e6a24ad476c13b90d550adc648693b764a7ecf0b15b83909","pubkey":"b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9","created_at":1612718177,"kind":1,"tags":null,"content":"I'm akovalenko, is there anybody out there?","sig":"0f31cb6077b169f4316c7db18ea1b8e0e6d6ac408d563ecfd75d26c2cbe6bd0f441f8cb6759d4cc13f4d5f13470a403dd77cfd55a104ce50f975a8a2d0f16a9b"}]
["EVENT","5496183021909538",{"id":"a6ac3698abfa3be971d038769abbe4448615ee07ff19c9f727a48cf8ccec993e","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612718265,"kind":1,"tags":null,"content":"hello","sig":"bb261862a595791c504ef3930a03d10dc2c784a5f9e9a912d948ec32617e0b2c6699b1bc3d8af3fa195bbe8f9a79d3170fb7b3dd66a1b0d23d43043bbce85b33"}]
["EVENT","5496183021909538",{"id":"08a572f46715562dbbd4ba6687786acb9b1c2a4d2e9b627f605f782482c174de","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612718310,"kind":1,"tags":null,"content":"Wow, got some data","sig":"f632c4485f8165d2323e2f11bca7a5d08b9b041efcabb1da76382b52fb703f44cf7d6aa4a0730d3e992d94b17237cb12cb87138685d431620a4e5062ef328e03"}]
["EVENT","5496183021909538",{"id":"951b0c82b9b4ba64762d948254f6a909f40891ad5535d4520df227ca439b6cfb","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612718384,"kind":1,"tags":null,"content":"I just see a ton of gibberish","sig":"8a5778393a927f172a63bbca5125725cab01f73c5d1d5e437c672e183fb9944006ac6d1c453f282e535d1f4c15878400999bf154eba7fd87a135759e07ce95f7"}]
["EVENT","5496183021909538",{"id":"643f09d78fda9b1747f701b4d9031a96a0ab8109fc67049d5303a92783e0fac3","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612718575,"kind":1,"tags":null,"content":"Yes, the rate limit is great UX","sig":"7784aae1d430c93b20a351a2e603a68913618ab68aaa39c1e05c619b986b69f55cba4552973564b08dbe51223e070ee765bfd377224f60d9278f267dacef52c4"}]
["EVENT","5496183021909538",{"id":"9beb6fdad93036d38a8c12cbefc39b4b9867b18e43b4329562be04ad3c292045","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612718709,"kind":1,"tags":null,"content":"Anyone tried this bot on telegram @zlibrarybot?","sig":"de19affb1494c650c980b9b7743ba1d90750e4ce6f757c2340eb6d50d7b2ae78d6db508ffd6508cfba256b76aa075085f04b42ce35ffe10434faf5ddee4b1d02"}]
["EVENT","5496183021909538",{"id":"36220ea30231dc02aca75151bb22097497cc32d86d091cb4dbfd2a27704ea463","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612718724,"kind":1,"tags":null,"content":"Just put in the name of any book","sig":"4eaa22c253de6d40f17b0b1a5fb9055ee2a533f30dfb0354ece8b19913b3231196d66c31cb5ea846629845995afa5340cea894dfed62dc34e21e6bb081598f56"}]
["EVENT","5496183021909538",{"id":"45880a8e7effae31debc120bf563894290ec0f6f1cfdfa18c37384297633ce99","pubkey":"4c26f43c256ae19fd025e6a619ae9b3ddc7092568d464613d94d05e6c573d4af","created_at":1612718750,"kind":1,"tags":null,"content":"Hi","sig":"eb9aa9cb0c341c3ed87cbe92b8e532226fd0fff6b93e5de67b4bb9329c479fd9611740cd48ff4e0529263f7a61cb7b48da075020da4cb980b94794083f5ae935"}]
["EVENT","5496183021909538",{"id":"e914eb8271ea891b45b9fbf7cdd01b3cf09861b9245d2f9935f1f2a9f1ffa690","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612718792,"kind":4,"tags":null,"content":"yQylpih6SrlVVusFQodcpQ==?iv=wl1KlrC5A5qulcTkzA/pWw==","sig":"3347c3d08dbccbcbed7dcc5ecdc7d8000ebf60f2e6f2d15ba85b11ae214703949f1d77614974532bfb2aae51189f0c20b7743b1c65a278cc4060ef9713c4045a"}]
["EVENT","5496183021909538",{"id":"3366fa6384aa042317f76656187cf4629225ed3246c5cd69a73e46a5a28760dc","pubkey":"359322282153a422b8f5e727acbd55d53f1e7a3aa76651d903c8f33eed316fce","created_at":1612718848,"kind":1,"tags":null,"content":"Bitcoin","sig":"302e913d3234356a5dde9b508cd9c614310398e267e86b1a77609a51a5a23a2e578a5d2a7f9d66436c62927c23c1272f307896f0a204c2c16285a3f69290e74c"}]
["EVENT","5496183021909538",{"id":"128f4f9eb1856893273ac91aa9544f02a9b7bc0e701a16449412f46e9f283db8","pubkey":"76c8658c827c8f4c1680be9f0927882c73000aac1ca4f70cdcc0279e39962309","created_at":1612718889,"kind":4,"tags":null,"content":"H5Fvj/EuZ2sKme8h5wZ+GQ==?iv=gpRePDQtugknxvORJ6PPgw==","sig":"87f1847c98d8776447123075f5139993ac3fd19738a55c359bd64acd2b0ad1d3ce12e1b04703536a488bc061e17719fb709ccc14cc109a95b2746ae527abe090"}]
["EVENT","5496183021909538",{"id":"ec39944536c3f9524c434c6d91da99c4dcb553fef16a9cc3ce2563ca47bef081","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612718919,"kind":1,"tags":null,"content":"so far so good","sig":"b1747003555faf4bc5310f20d27f77cbe1877d31c64cd555701153d149b5799ca92bf38efcf89cf336e005627620d4063f497c3f9a9abe278e3a4329bb32494a"}]
["EVENT","5496183021909538",{"id":"3fa4586a8d0bc29c236d07a553dc4204a0bd51ae13adaa9539e0ac66b1b64009","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612718994,"kind":4,"tags":null,"content":"PPh2a0C+4VBpPydvH114kw==?iv=pCny9BX3pzh381QMODZfSg==","sig":"b0d4258ca804e1142a9735d075c73457b9ab213eaed1e8336046ae4d9b422cf74178e3b0e640b64ddeb40584f600b236d2c81f19f51b838fdb69938f077e991d"}]
["EVENT","5496183021909538",{"id":"a724ac4709629a4554d4a3b392a71beb5a0f365288aa6b1a8057957ec3c9b2ed","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612719055,"kind":1,"tags":null,"content":"ok, I've removed the rate-limit from my POC relay code","sig":"0f908b235639180e4e906c27a5ca0b5efa97a746c05ada7c48341a144bff2919f3bef692b3894ad33dea494c0dd9c15aa83f5011c1f0b77656016dccfd1d0f67"}]
["EVENT","5496183021909538",{"id":"8e476e2a1d9c52cc06e6f2ec2d74d5842f84e3e00a2a42191c095f6e62706de5","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612719085,"kind":1,"tags":null,"content":"but I still think clients should be aware that relays are not to be trusted and they may fail for weird reasons and so on","sig":"d9b80145fde32ffbecf0456ce69b1b7a55df0d5a01210dbe3a4c22050171aa61d5cef9e202d121987d07590c84124fb4e4c566235ec9f28f4c5f783c0ea5717a"}]
["EVENT","5496183021909538",{"id":"6ccca21b5176945cfc303825ce18aea2e3023680375d8e57788190399b09e8d6","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612719122,"kind":1,"tags":null,"content":"I get a lot of random relay errors","sig":"5c60bcc689fccc32662ae62004c22f81ee6d3ea6dac0e603aca2ef807a2b7206fc0ffb7c793e5930c13f0eeb55de5fca5cf9ff00ba8840bdcc52dfc0248ecbba"}]
["EVENT","5496183021909538",{"id":"3c3441e03c9dd125b5b4ea0d456ab731653e888c4a4961958cca832be388cb46","pubkey":"2a6becb02af9a9c16f78473bec828396ff39856e6ebbee8ff9a40450d5585a71","created_at":1612719134,"kind":1,"tags":null,"content":"Hello, World!","sig":"19639912ff30544da35da60674f830888ef2139dc7c9206003d440a859a02704f19287a1f3b23814d0485a69a6342e24583a1b0064c45f32d0563786b3c2fe01"}]
["EVENT","5496183021909538",{"id":"d977fc22309d501976ab9a317ec59d88802bbdd8cac2d1e0fae24b0b1993c8c7","pubkey":"8fb2b4dfbbad68a3001d3e25f745de57c75647e8cb9ae0c3cc50f297933e4337","created_at":1612719190,"kind":1,"tags":null,"content":"Hey Nostr!","sig":"3c09e5737d588b498454581dba7a1ff972f3d34bcd39441ba5421338d4b05de16507b07402aa7d2649a7bebc571bb1e9769877df24e56ca7d44732cbd6f2e4bb"}]
["EVENT","5496183021909538",{"id":"c3ea79a52feffa76b10f26c5041b2493545b27668bcb9abe0cc4348acf0852f0","pubkey":"8c79b25dda02684757031cc31c05ed8c17f5d6111b2b80f825945b838450481c","created_at":1612719317,"kind":1,"tags":null,"content":"*insert Jack Nicholson nodding meme*","sig":"ddda774b976833aa507422fff17d2090814be63db3580aff8e83822aa9ab892b7bc58a3cdecf06821a8ba37ed9b30bda15682d46cd24e7537c2c4616d8d91fad"}]
["EVENT","5496183021909538",{"id":"9eb12cda084c1e6692e9421c803c8c4970a658b223c28eb7af35933a246076f6","pubkey":"6cb51a92f6c1ecc613db13fca5da2dbcc23bde6613eb9ea7cb79686fea6ca221","created_at":1612719361,"kind":1,"tags":null,"content":"Hi","sig":"204ba930638a2ac6ab7c8eaf264a529a589db2f5ff8f54076e55084edd9d65325bef86f5f65ec23aacbb0190bb2ace4624d9fe33b5815a6113325088fecc5f59"}]
["EVENT","5496183021909538",{"id":"4de66a382a962f28c755979b85118a5a940bc118742fbc6fd98dc9260ba437c8","pubkey":"8fb2b4dfbbad68a3001d3e25f745de57c75647e8cb9ae0c3cc50f297933e4337","created_at":1612719388,"kind":4,"tags":null,"content":"+jEjChxHpBFL9YtMzMv6Sw==?iv=Fc8TB1CFkOEV3Pd2BoBhSQ==","sig":"a666adcb630fe810a75e523d8e80295355f259c22e5818a1703bee1556217cc932d04d33c7100dffef01dbf120888b5221001d544ed01bbf0cc6f20c572b471b"}]
["EVENT","5496183021909538",{"id":"635ebf9aa3adf665bc5fbead6b45e1bd3d8933d2c3fea82989de2729eb352f66","pubkey":"6cb51a92f6c1ecc613db13fca5da2dbcc23bde6613eb9ea7cb79686fea6ca221","created_at":1612719522,"kind":1,"tags":null,"content":"Hello","sig":"3b07c57ddbfb281c39b4d9e238e760f3e7f8c81e9d6b4fb960ed3005bc4de4c501782d8e926ac5039f732d010a462aefaab83b08caddc1fc4b2089e6a56f37cb"}]
["EVENT","5496183021909538",{"id":"033e38ba1e59cadcce55434d9d416406f774fead3fce93226c8fbca9e919005c","pubkey":"271cbc18d71f9eeaca6693dcbaf76a54730e80c8dc7d2b11d5d1b242121f1555","created_at":1612719720,"kind":1,"tags":null,"content":"test ","sig":"c3a7ee3a08a948364ba00b490685516683dc6b6b283fb2ed013a6d08c231f82aec4ef6c876e0c46c161a05794c9dabf128bc8e2841b8ce74e20dfd285d648def"}]
["EVENT","5496183021909538",{"id":"37ab40788c8ac4e3a6dfe2be16647fb68122fb9d3995e2f39675e0dbfd1bc36d","pubkey":"8fb2b4dfbbad68a3001d3e25f745de57c75647e8cb9ae0c3cc50f297933e4337","created_at":1612719782,"kind":1,"tags":null,"content":"Hi, I'm Elon Musk! We celebrate the TESLA anniversary this year. We give away free BTC just send your BTC to this address and you'll receive the 10x back!!!11!1 Guaranteed!","sig":"89cdb3791e845b42da1326415e0602dfb18551c7a0f8b67f2c250397e52c1b500036c3e8e40e844836eb9b868ff105d065e0cf26b2777e02589a47e1cc88cc76"}]
["EVENT","5496183021909538",{"id":"463c161858ef6a6f81a702cba1b2ed18dd8e4f13ca5a02e6abb2d912b7248cae","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612719841,"kind":1,"tags":null,"content":"I deleted local storage, got my feed back, got dms but lost my side of the conversation","sig":"9bb358ba036273dad1bff21d6aec38d47f5026325e849db31224cfc579e2fc3f8c27a3a52dbc85cc2efd86a10e041e2fd8c4868fa842b412dad63f85367ae344"}]
["EVENT","5496183021909538",{"id":"21f6b8d34b297e56f8b1df778a146beeb441dbd0209f7eb798e7c0dcf7ff745d","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612719895,"kind":4,"tags":null,"content":"AM20BSQKE8B7S3P93h84KQ==?iv=RTEAOmRoDKdR/pJ08KmJrA==","sig":"fd9a740788fe1bc67de55ccaaafc487cbc879988fa4ebe900f0751f824d75863e1d4c28b29d3054f419a4492fceab17732b1c7311ba8ac2cc2e7106e55b9040b"}]
["EVENT","5496183021909538",{"id":"77468731e429b95e5e086f58c9d2a30c5631ea3800b3e2c359890d151e06158e","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612719901,"kind":4,"tags":null,"content":"nSpVaB74d6Fco01YFdLIgg==?iv=3/5tClR6B7ZdDcEGf38Aew==","sig":"84a32b0ab26f5d9d47701ab189af3fc9331ba8df90665bff94c3d222205810c05ece0abdeccbcb26ede41b7bec6f557d057d37f3ce571d3c65541f66deb550fd"}]
["EVENT","5496183021909538",{"id":"bed9c3e1734b93e1d6689646fc692f7605e6adeb4bca90ccc161e6fb7557c275","pubkey":"97d90ee5725feb457a9ba7969acdc92049b2f16471e6b50596c378ebea48ee55","created_at":1612719940,"kind":1,"tags":null,"content":"Wow, thats pretty cool","sig":"3207436a396788597d6ca7a2be4bba2b9c480aa208c374526b8001276cbe0dd547fb594224edb848c238724ccb9df76059ffcc6c792653271f1140b470ed2385"}]
["EVENT","5496183021909538",{"id":"03e0972dc6a94e88ee7bc1cdb5ba10bb67122f5d9d57bed0d99847d670773214","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612720554,"kind":1,"tags":null,"content":"Test from a new relay","sig":"0d927de5915c12de773e17ef4db3bfd39977c937d97631b8f0b82d723feafef13ce4e96d95992f02e93dea662ff82aca2adba70ca48b1c7c5c780eb01dea5bff"}]
["EVENT","5496183021909538",{"id":"38d696d8b96200e10f1335f83cc2ef40393021336cc9cec254b7c74b97c8cf84","pubkey":"5f1056c13e6f188a64963d7993bf45eccfb4108b839c579c480ea94d824e4712","created_at":1612720654,"kind":4,"tags":null,"content":"TzyTqitK+Adv1cgSe1trDg==?iv=ELXw7Ije8Q074chj+uGS2g==","sig":"1d706b49307a172c8db00b475b7abe9f159b088dae8d054fb40c0fd7aee3763897ecafb4dfeda065c2ad1784078c07de7597b6fa88286a0697c9c74606f6c186"}]
["EVENT","5496183021909538",{"id":"35f9708ff0bae02ce4fc3799f50fcaf1affac7ba809cca9e18a2bf70c601a0db","pubkey":"5f1056c13e6f188a64963d7993bf45eccfb4108b839c579c480ea94d824e4712","created_at":1612720665,"kind":4,"tags":null,"content":"todzDCDEUecb+H4fmkiwzA==?iv=7ecvtK2r/kOyh6fyD79inQ==","sig":"f7dade434bec0c43b68397893a8c06c439f8ab628a347381cb2b509ccbb0ab3af96fa50dcc81166cb0781d408e8328f207ce05e73503164fbca1702d9ac4d837"}]
["EVENT","5496183021909538",{"id":"e5130088298114e5b3cda517dbfc76999ddc758e3462db28236b2ec8deaea84d","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612720777,"kind":4,"tags":null,"content":"+sEvKff5h25ieydxbT6V9w==?iv=3IfPlCKjWN1qKACBrikGdg==","sig":"e00183869031142455c10722b6b9c80e7de0b7c32aef743af102ad4621df83583cf68b1686f9ca6cd6ccfc6d9b9dafca632d10ebf7600548ce6af83cd968ae3f"}]
["EVENT","5496183021909538",{"id":"3b7444dafaa8ba360028d1ca352558eec138380eab6d41f269a90f53f2d1f76a","pubkey":"6466df2abb1b1d0114305d0a11397411871a2ed1b133b7bc8299f958b65fa491","created_at":1612720778,"kind":4,"tags":null,"content":"KX321Ey2RZN84rro05lxLw==?iv=WK18NePy+Kb+TCYmH+j7Gw==","sig":"0fa034dba846c0a459fc8943c7fd93f72baefb8af9cb8ecc17308e8f9b7a45f2a839dea46f90df6f5cfa048d2e02e9b82de43820dfe6ab4e1cd1611fec4f91cc"}]
["EVENT","5496183021909538",{"id":"257ddf06fe141133fc3278ea3caea681983a128b2017c1a7310330b7f3b6685b","pubkey":"da5e9e12f0ee37574fe8875e95f53057a7d9345ad82b692712a0c2c9a2a53c02","created_at":1612721689,"kind":1,"tags":null,"content":"Testing...","sig":"c6a51c19e6f1203801f16a4999dc3e6c172925fd4767d3dce818c38b41cdeeac788b0048fce42b9e09a5bb9bb3687c257a7f72920959187441c2ccb6e6774705"}]
["EVENT","5496183021909538",{"id":"f4d3a6d1287a75266c5afc1672544bc3157de1ccecd15bf843202d2bfb218a02","pubkey":"2a6becb02af9a9c16f78473bec828396ff39856e6ebbee8ff9a40450d5585a71","created_at":1612721718,"kind":1,"tags":null,"content":"I didn't find it in the mobile application Add public key. 🧐🧐","sig":"b69226f5b17b1a610e81547f78f117419bcbff4230eb2ed745f5c3bffe3575ebdc170a580c2c4ad055c53a941481ff5906f60167d48cb02f93b673eb950ed919"}]
["EVENT","5496183021909538",{"id":"4db57707d52e788a2d5f37fce5c74a7a67e82f102d11e20ea6d315b21e644cbd","pubkey":"46635e6e168d3daa4961f854f688428c5c4dc47faff5b4e2930f8c2394e0b6df","created_at":1612721776,"kind":1,"tags":null,"content":"helloooo","sig":"23f038a45cbdf38b002eca22f7d6e862f2e57c37e2d79b80eb4cd629777afd0e03bc1edd54dc8f8921684e8f2a17870d56c3598fb50e0231f5a481d9c92db3cd"}]
["EVENT","5496183021909538",{"id":"12ba7e31b8f7e9aa2f210f03dadfdecea331ab3f8ead3c7a1cbf8f4987ac11d9","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612722130,"kind":1,"tags":null,"content":"my relay is fixed","sig":"7faeee90de41eb67bce29f03d0c1ed98024e9f4f83b9875511dccfad4fe2dc9370ea8099353a13eb54b2411778a49fd3c70b83fbabdb0b4c4f673fda07a8979c"}]
["EVENT","5496183021909538",{"id":"c782268e60b28e3b01fc094e59716753aa6d11085b4a0c43c1f7999c9df4fb24","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612722180,"kind":1,"tags":null,"content":"I am `systmectl` clumsy","sig":"230748aeb559d1d9b9b4aae3b74fd9b5e371adfcc9a80d9b67c05d664204207bf1f440eb4a8b8959b43bbe1789bbe2b762e4aaa94355d0258ebea518a6cda0d9"}]
["EVENT","5496183021909538",{"id":"762ad4264bcfc5594d495ca737a041c92f83206b8a65d57c33afeaa717697ef7","pubkey":"b91268018358bd83c80b972c1c955550294169ce64a4daf6d5b32ab9be02843b","created_at":1612722208,"kind":1,"tags":null,"content":"I think deleting the scroll thing worked","sig":"55138849fd4debabaa9432aab7bd3f71ddf79c1c33a8eaa7549ae42c173ae0a034ce729d6a715092cd79615ffc398dc36d499ced631ff69dfdcf63cea14379c0"}]
["EVENT","5496183021909538",{"id":"97c41b68d39f383c5820bea80cfead8066b42cb94d536b75f1601159c999ba3a","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612722277,"kind":4,"tags":null,"content":"oWSXntrVNx4bYaX/jvQ00w==?iv=UP+cLHDTS7FeleAVbn542w==","sig":"8ecc4a01011292e88d9b01917c3934ba1035152edec5b56a33a3bec56c3ab0ba357b3976ccab868df5ef73413962b15c957e70ce6c5e325d0f5ac82202ea4ade"}]
["EVENT","5496183021909538",{"id":"1f868ba3e13c97ef0f2d622d2e4e091d53652305aa542ef7f5cb07790e57d62d","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612722401,"kind":4,"tags":null,"content":"3kS0ICsW17CGSrXFD6MjDw==?iv=Cm3hZDxNSMCp6LzgesXU9w==","sig":"3726851fb326210d3f5ad20efc2fb636a8bfef515a41fd55d82842004e2b0e6fb3d79f58af1963311c0e2a753f6373296bd97634779a593e953175716bc4bef7"}]
["EVENT","5496183021909538",{"id":"a23bee31c65324a70c1696533040f1ad13a29d49ea7ac3faac6f7d4f490f0015","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612722420,"kind":4,"tags":null,"content":"HaE5TWUEbtI8+gcHcwccNg==?iv=9nE52WRDvEv92RKUt8/iCg==","sig":"3ce2014c6a605ca785c1315a2620b357e19ad8332c119ce392dd260b42c59552d4a157f9e367ee88d0f55994805770cec4c5947b26847d747ba2f7f50613cb43"}]
["EVENT","5496183021909538",{"id":"91b6bd660a5c736490585c5035d6b4232723175276fa7fcb20de4d6ff2d488e9","pubkey":"b91268018358bd83c80b972c1c955550294169ce64a4daf6d5b32ab9be02843b","created_at":1612722902,"kind":1,"tags":null,"content":"Whar","sig":"b3c0c1c6c2cd5f2acaf2fb5664d514dcb5a7347c9871dc342a3a8db10364c0c18785573b13f8107fa36e336566e2d155da735979c359996c65c9abf40909ad71"}]
["EVENT","5496183021909538",{"id":"43938617983503dac8789a5e1328e9e6cba630d0038935130b6a08013199d47c","pubkey":"b91268018358bd83c80b972c1c955550294169ce64a4daf6d5b32ab9be02843b","created_at":1612723064,"kind":1,"tags":null,"content":"What next then","sig":"d16d703d7919b8170acb25a7c68e2919b46723575abc017d9cb28273c8387e4fb6a31385db07ff89ee07f2c82018768e0ad052d72a6a6d9f289e45fdba58d140"}]
["EVENT","5496183021909538",{"id":"7fb1d4d16fe68d0b1fb5c5c229505e8f581f8f1e24dc19ca87ba712224ce500c","pubkey":"b91268018358bd83c80b972c1c955550294169ce64a4daf6d5b32ab9be02843b","created_at":1612723635,"kind":1,"tags":null,"content":"Scammer","sig":"a5247f0b185a50ec1d13a776b52394c24e07a668caf8ce4c20402ef23cf62a9b362cf8c18225fea2b7708680a373855e744fc09933da53ded662dcef68fc4d9c"}]
["EVENT","5496183021909538",{"id":"d4223bb3e51a2e79b7e1f71707d7dfa3572fddd97c99e9c231e636e7d217b345","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612723646,"kind":1,"tags":null,"content":"I am on the netlify instance now","sig":"f4aafcc264ae078e5faded7af12f49be4db4fb28beabb48a837fb8b7a9374080b881022784eac187d4736e48b7865c9be2f7e3fa0e5b58a3ec4b02d25e738186"}]
["EVENT","5496183021909538",{"id":"7b9db98b22b9d33d8782c2be56265c03fbc5302118cc80634d92abc2412489ce","pubkey":"42142dc712ff8d1a4aa0a3f6990a60dfde6a19fb872e6b98d50383b8f36d6491","created_at":1612723968,"kind":1,"tags":null,"content":"hi bitch","sig":"6cd6bd473c7319a57ec687d77b1f87bf515eeaa70b2559d838b3d7407b576b96b779c73449eef7ebcc63302bec4bc841ad88445ed734eeb1e4b8a122857288c1"}]
["EVENT","5496183021909538",{"id":"13586d7130e6cc14a684c4ab1ad578e410183cfed8be34f48dfdc1dd02259ce9","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612724128,"kind":1,"tags":null,"content":"netlify try to not break","sig":"618109dfd3734f7247fc7702f5e6198509785d5da0d9328eb6d42f5880ddc75b056e5f9521a424e3759a44f0b64300c22557fd1e7e9f89c35f0b0e6edc51035f"}]
["EVENT","5496183021909538",{"id":"9a0707d478c45ef06eabc412ce05f7e7bffd260d509574bfddc3f55206daac63","pubkey":"34a229681f989d0357a60f89600f150f30b8ca5879a8e4b6fe29b451d3eec3d9","created_at":1612724140,"kind":1,"tags":null,"content":"hola mundo 😎","sig":"db08c883ce6adc74e09eb8eab86f5f013bcbcbfcef247e43c3ba87d06dac504163481e76820574c704ef9414af5b1e39bb1c3907dfadf60f2f79c865763d5d07"}]
["EVENT","5496183021909538",{"id":"2f26cb9643e1ac11c4933785c412a15b61267be3f7141817f44df3fa2acea8f0","pubkey":"7859989f62f49100fb71fce64ef03e0a89fb5258e866e7e70fbdfa83d09d016d","created_at":1612724287,"kind":4,"tags":null,"content":"2g5kgiiGNmetO4p6SdQd4g==?iv=naRaO8xDiGtUyNDeuUuH1Q==","sig":"3a414f0df3f28f0a1a93741b6cc30e4966967adde6fdb8bfbd816051af39d3d5f208d11f610952208917567b276037986187e103458bd8caf42e0a75632b22fb"}]
["EVENT","5496183021909538",{"id":"8e7ede8bdd4dc1ce73b242e9769a28af35025261e5c15c0b8e390973bd87619e","pubkey":"97eb9cffb7ed45bddfcfeef4ed158509361f459df5e5dc9a5e20e6e9e7f3292b","created_at":1612724442,"kind":1,"tags":null,"content":"Running #Bitcoin","sig":"74f126533fb2c127e64936923323a9bea47d5835296f45c52f244c3268234a942d6ca94c247585fb94ad375afe711b88a3a7fe75138280e96520d07859aae7dd"}]
["EVENT","5496183021909538",{"id":"920a36e97fa1e6efc720eb2d633e9aed1c525bdc90a69b1c9edf4f4473d85d08","pubkey":"38ffee9a31d0ef9d4e34f8af52a63fd569535edf33b29475b17ba8a05406cd5a","created_at":1612724458,"kind":1,"tags":null,"content":"Hey","sig":"c2ec9ed12eaf40d7b12a6973aabb861e23cf90ea6c51ef321544d4d9a405b1f895426a1a3036a6d475f46968539e1e681b8839ad30bed85679c5c4f56cc6d5a9"}]
["EVENT","5496183021909538",{"id":"a0cce98db0522ffb2416db0805d91a03834fcd15181a9f449e36485c61fbae80","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612724556,"kind":1,"tags":null,"content":"netlify should be fixed now, mostly","sig":"4dfe55db62725b39054055eb1de844373b7f21ed3274531cf010542bf448ac8b842a719af171db1f12842be441c5cfea2b3b295150e38c7b6e1716fbc20a10ec"}]
["EVENT","5496183021909538",{"id":"3b9467414a319b4331c96375e2d1b553508799fd83bc3b83de26066a7aa395be","pubkey":"b91268018358bd83c80b972c1c955550294169ce64a4daf6d5b32ab9be02843b","created_at":1612724888,"kind":4,"tags":null,"content":"gxJTObw4DhMe5r/b5CNnPA==?iv=FTPzPR5sbzCqmYMZ5TA4yg==","sig":"4a75cce11b98ccad8eec1b35352931433e48f44b2ea592b7c8dc42003738e19bdd68a72e73996940aab21d2916722b80f61240cd7730ace135c366549f98e102"}]
["EVENT","5496183021909538",{"id":"d8683498da8ea9ccabf0a7b713d134fca696e809b5eedd2d2e792139c0fc5693","pubkey":"b91268018358bd83c80b972c1c955550294169ce64a4daf6d5b32ab9be02843b","created_at":1612725164,"kind":4,"tags":null,"content":"2bK8qPakfVGmuR5YLQ86qg==?iv=tGyApLaIMpjNhckLINqnCg==","sig":"4e3590ddd80efdeebbd5fd36eb7049447ef1fecf04763be71258dcec0d3e99a6d39f3ef12eaa9479fc1c7e7b38f73d50ad44beee01cfc9ae26e1a0e417a8119c"}]
["EVENT","5496183021909538",{"id":"d50f2b453128dcd3f3fb1ab0504d817f5ab657be8105b78fde2bd98b9ef3ec40","pubkey":"e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216","created_at":1612725187,"kind":4,"tags":null,"content":"IhxR3DUK6cuusvxJaXQjYA==?iv=QywipN395oAmSpPo3v/ESg==","sig":"81d139cf9f9df56f560b92bbbc5c81f4a24b85358986fd05d4008bf4d7e9d505400191adaadfdb81ae0c06b4371b63a21ab09939f6fcdaa2c58bbf43afe13c85"}]
["EVENT","5496183021909538",{"id":"e7cd845b72ecea0e409a22526c273982b5db3c724259152c80c704781b0515ae","pubkey":"08cc733d6660e1df8677f17cb149d177a2e19bcc51cb3993234e9ec51eb50a65","created_at":1612725385,"kind":1,"tags":null,"content":"Hey","sig":"11ddd49be7ab14fed60dd7efea317a25fdcac9265407e8865a7956fad56cffa6bf0ceefb8bca5fa86c99e29263f5fad1daecf439ffc03bc6a5819d14782ab6cc"}]
["EVENT","5496183021909538",{"id":"6bdd3088c100f71124588dfe4806993e248dc6809f52cf13606f4f331b6e9b42","pubkey":"5f1056c13e6f188a64963d7993bf45eccfb4108b839c579c480ea94d824e4712","created_at":1612725524,"kind":4,"tags":null,"content":"RVz8i1lgi1iAh0IfQcZdkw==?iv=teFxtIOFeboJkXZl9InubQ==","sig":"a68737ecb7524a9c840face76af714934487564189bdfeb3668f5dea5212c25534a9770319722adab4a4eed9b9be20af62e0208ab71d1f6a456edfdc3136f7a7"}]
["EVENT","5496183021909538",{"id":"75e3fafb8cb2b949e67a938cb5edefd2dfc527e47a5b439348130eceb131196b","pubkey":"5f1056c13e6f188a64963d7993bf45eccfb4108b839c579c480ea94d824e4712","created_at":1612725528,"kind":4,"tags":null,"content":"RiL/bP37fcHeuhkZT5hc7Q==?iv=bJ+5UxnBFDrWMW33TA7L3g==","sig":"bc99ae69cacaf0062ff57d8c66a0f329c5f33bacf741144e467fcd9ddfce8fb4c38f8e60cab3a557f6ee6950d8737e99935fa00c234d47047de3d82672c4e128"}]
["EVENT","5496183021909538",{"id":"4cfe3bdb4c706339257710d4312b2ef3825aff4e0bb5cbe3d51e62ff2308fc1f","pubkey":"5f1056c13e6f188a64963d7993bf45eccfb4108b839c579c480ea94d824e4712","created_at":1612725730,"kind":4,"tags":null,"content":"dYeM0n1w5gClZxvP/+BGiA==?iv=ddKhdGnlsQ6NKUQ+WOaPpA==","sig":"0b25b5696d8e79c758e66129db3de7d1dbc005fd416ecf31decfb8b0ffdc838528fe9cde3d520225f4d4fc963787a42f65e8c474ae6d935a375dc04603cc2636"}]
["EVENT","5496183021909538",{"id":"fa7754c8e6d3ce77ec788a585385563582ef8996afb61eb66eaf9ec20bc481b0","pubkey":"b91268018358bd83c80b972c1c955550294169ce64a4daf6d5b32ab9be02843b","created_at":1612725784,"kind":1,"tags":null,"content":"Well that was trial by fire 🔥","sig":"27d8ef4439e09eecb52c56886bdfd3882d547f34a7de05db99b174163be15fc016523fd31871ef630c2aa31e0dab0baefc0575be51abf2e373918d1e2ff55973"}]
["EVENT","5496183021909538",{"id":"7b32445285e3d59c66ff74031370d506303c1cc254f361e1dd1ba0ac8660f140","pubkey":"5f1056c13e6f188a64963d7993bf45eccfb4108b839c579c480ea94d824e4712","created_at":1612726042,"kind":4,"tags":null,"content":"vuc8VIl0xz8b5kNIGfs0Ow==?iv=2KKjOb05Cd3kMn56PYEMLw==","sig":"14f9ed8ab8a811f8def79370c23be55738418e380b28b76fae5c09b38a34fb8d98fe724ff78eedcea280aad4b3c1200e9956c1c414abfcc813614d715191e8de"}]
["EVENT","5496183021909538",{"id":"a7e8da90926dcaf9fa4a0897cc07c724f27b0f7725d538f807c6ee6108905ddc","pubkey":"901d6d9cfcb94291b8be03d44895224fc26c024c79efca6e6daa04589956887e","created_at":1612726051,"kind":1,"tags":null,"content":"Test nost","sig":"89fe7cea6e2ef532fcbeb1e25cac78c8d6d539c923d08ad75873831f6b281637ba440bf714f62f05e7f2bcc92b247d6b7bd1b61c03bab306ecfc02fd83791554"}]
["EVENT","5496183021909538",{"id":"c5a6dcf0a1f9bf5072189b60e8e8b3f46b0ae862232de95b575424db9eb5b536","pubkey":"901d6d9cfcb94291b8be03d44895224fc26c024c79efca6e6daa04589956887e","created_at":1612726088,"kind":1,"tags":null,"content":"relay says rate-limit","sig":"62a0432144584e6dd37d1717f35e9a7d3a1c6db2a2d8e490322009946614c00fff9d7f2d39b0822f0abffab79ab70f15f26d3d0f26e1c742c7d17cadc772811a"}]
["EVENT","5496183021909538",{"id":"002afa98c0293fa7b9846934d2fc9d0d4508d9e7767d663d25fd3cd2aecb283b","pubkey":"b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9","created_at":1612726313,"kind":4,"tags":null,"content":"NhLGT83vTqBayskoh6V4Aw==?iv=XGd0VFUVIri7ml9rRJC33A==","sig":"00307cf72b7b467175f5c0d29bd78af1a702589b7ecae0a9634c8f2fcc6d8391017127f46229b637bb44ed91d4e0c29361b70cd1e8c8e5036c30550616860ff9"}]
["EVENT","5496183021909538",{"id":"75b58a2b7992fbcad1ce996ac1ad5daaf72b495063db6ca42c0b70c2ced4b5d1","pubkey":"b09f278c6551c398866bb408f9b34817cdc0c4a78582387a7ba62d2e97036491","created_at":1612726316,"kind":1,"tags":null,"content":"if anyone is DMing me I don't see it","sig":"02f8529deaf5cfa8c717ecd51f421c2ec0fbf58a4f1c66fc8462f678aa22ef49d497dd21bdd414899a8fb355d6808e325097ebe90a4641fc4a3ba8f5b60d1a57"}]
["EVENT","5496183021909538",{"id":"e61e00cfee8917f0266b6b51e2888342d66cf2e416f155083a0bdda4b35689a0","pubkey":"b91268018358bd83c80b972c1c955550294169ce64a4daf6d5b32ab9be02843b","created_at":1612726363,"kind":1,"tags":null,"content":"Do you see this b09f2...","sig":"90f12736588764f22c0c75aa1f3d6fe58dd4c22b897d32659c5cedbd0b73315e8c41bae01277c43442997f91053c3acf1c968284df0053a3976c56393189138f"}]
["EVENT","5496183021909538",{"id":"b1b6fe662f109c4d60512c2e23a7c4e70c9b536a173c850efd68167779a7c0e4","pubkey":"5f1056c13e6f188a64963d7993bf45eccfb4108b839c579c480ea94d824e4712","created_at":1612726530,"kind":4,"tags":null,"content":"oidImNefkwTgiaCLZ/5veA==?iv=TM30ouqWWuSDIjgnUZvfMA==","sig":"b9be71cf72afb23135534d118abf16eb7cd0c2053159a5bbd9b4bb0e910f82ae8673c1cc4c6f0cbe614e2d30de3620e5337fb7d37edc6273c2916f6ad20af64b"}]
+52 -20
View File
@@ -1,32 +1,64 @@
//! Subscription close request parsing
//!
//! Representation and parsing of `CLOSE` messages sent from clients.
use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};
/// Close command in network format
// Container for a request to close a subscription
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(transparent)]
pub struct CloseCmd {
/// Protocol command, expected to always be "CLOSE".
cmd: String,
/// The subscription identifier being closed.
cmds: Vec<String>,
}
#[derive(PartialEq, Debug, Clone)]
pub struct Close {
id: String,
}
/// Identifier of the subscription to be closed.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Close {
/// The subscription identifier being closed.
pub id: String,
impl<'de> Deserialize<'de> for Close {
fn deserialize<D>(deserializer: D) -> Result<Close, D::Error>
where
D: Deserializer<'de>,
{
let mut v: serde_json::Value = Deserialize::deserialize(deserializer)?;
// this shoud be an exactly 2-element array
// verify the first element is a String, CLOSE
// get the subscription from the second element.
// check for array
let va = v
.as_array_mut()
.ok_or(serde::de::Error::custom("not array"))?;
// check length
if va.len() != 2 {
return Err(serde::de::Error::custom("not exactly 2 fields"));
}
let mut i = va.into_iter();
// get command ("REQ") and ensure it is a string
let req_cmd_str: serde_json::Value = i.next().unwrap().take();
let req = req_cmd_str.as_str().ok_or(serde::de::Error::custom(
"first element of request was not a string",
))?;
if req != "CLOSE" {
return Err(serde::de::Error::custom("missing CLOSE command"));
}
impl From<CloseCmd> for Result<Close> {
fn from(cc: CloseCmd) -> Result<Close> {
// ensure command is correct
if cc.cmd != "CLOSE" {
Err(Error::CommandUnknownError)
} else {
Ok(Close { id: cc.id })
// ensure sub id is a string
let sub_id_str: serde_json::Value = i.next().unwrap().take();
let sub_id = sub_id_str
.as_str()
.ok_or(serde::de::Error::custom("missing subscription id"))?;
Ok(Close {
id: sub_id.to_owned(),
})
}
}
impl Close {
pub fn parse(json: &str) -> Result<Close> {
serde_json::from_str(json).map_err(|e| Error::JsonParseFailed(e))
}
pub fn get_id(&self) -> String {
self.id.clone()
}
}
-233
View File
@@ -1,233 +0,0 @@
//! Configuration file and settings management
use config::{Config, ConfigError, File};
use lazy_static::lazy_static;
use log::*;
use serde::{Deserialize, Serialize};
use std::sync::RwLock;
use std::time::Duration;
// initialize a singleton default configuration
lazy_static! {
pub static ref SETTINGS: RwLock<Settings> = RwLock::new(Settings::default());
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[allow(unused)]
pub struct Info {
pub relay_url: Option<String>,
pub name: Option<String>,
pub description: Option<String>,
pub pubkey: Option<String>,
pub contact: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(unused)]
pub struct Database {
pub data_directory: String,
pub min_conn: u32,
pub max_conn: u32,
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(unused)]
pub struct Network {
pub port: u16,
pub address: String,
}
//
#[derive(Debug, Serialize, Deserialize)]
#[allow(unused)]
pub struct Options {
pub reject_future_seconds: Option<usize>, // if defined, reject any events with a timestamp more than X seconds in the future
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(unused)]
pub struct Retention {
// TODO: implement
pub max_events: Option<usize>, // max events
pub max_bytes: Option<usize>, // max size
pub persist_days: Option<usize>, // oldest message
pub whitelist_addresses: Option<Vec<String>>, // whitelisted addresses (never delete)
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(unused)]
pub struct Limits {
pub messages_per_sec: Option<u32>, // Artificially slow down event writing to limit disk consumption (averaged over 1 minute)
pub max_event_bytes: Option<usize>, // Maximum size of an EVENT message
pub max_ws_message_bytes: Option<usize>,
pub max_ws_frame_bytes: Option<usize>,
pub broadcast_buffer: usize, // events to buffer for subscribers (prevents slow readers from consuming memory)
pub event_persist_buffer: usize, // events to buffer for database commits (block senders if database writes are too slow)
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(unused)]
pub struct Authorization {
pub pubkey_whitelist: Option<Vec<String>>, // If present, only allow these pubkeys to publish events
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub enum VerifiedUsersMode {
Enabled,
Passive,
Disabled,
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(unused)]
pub struct VerifiedUsers {
pub mode: VerifiedUsersMode, // Mode of operation: "enabled" (enforce) or "passive" (check only). If none, this is simply disabled.
pub domain_whitelist: Option<Vec<String>>, // If present, only allow verified users from these domains can publish events
pub domain_blacklist: Option<Vec<String>>, // If present, allow all verified users from any domain except these
pub verify_expiration: Option<String>, // how long a verification is cached for before no longer being used
pub verify_update_frequency: Option<String>, // how often to attempt to update verification
pub verify_expiration_duration: Option<Duration>, // internal result of parsing verify_expiration
pub verify_update_frequency_duration: Option<Duration>, // internal result of parsing verify_update_frequency
pub max_consecutive_failures: usize, // maximum number of verification failures in a row, before ceasing future checks
}
impl VerifiedUsers {
pub fn init(&mut self) {
self.verify_expiration_duration = self.verify_expiration_duration();
self.verify_update_frequency_duration = self.verify_update_duration();
}
pub fn is_enabled(&self) -> bool {
self.mode == VerifiedUsersMode::Enabled
}
pub fn is_active(&self) -> bool {
self.mode == VerifiedUsersMode::Enabled || self.mode == VerifiedUsersMode::Passive
}
pub fn is_passive(&self) -> bool {
self.mode == VerifiedUsersMode::Passive
}
pub fn verify_expiration_duration(&self) -> Option<Duration> {
self.verify_expiration
.as_ref()
.and_then(|x| parse_duration::parse(x).ok())
}
pub fn verify_update_duration(&self) -> Option<Duration> {
self.verify_update_frequency
.as_ref()
.and_then(|x| parse_duration::parse(x).ok())
}
pub fn is_valid(&self) -> bool {
self.verify_expiration_duration().is_some() && self.verify_update_duration().is_some()
}
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(unused)]
pub struct Settings {
pub info: Info,
pub database: Database,
pub network: Network,
pub limits: Limits,
pub authorization: Authorization,
pub verified_users: VerifiedUsers,
pub retention: Retention,
pub options: Options,
}
impl Settings {
pub fn new() -> Self {
let default_settings = Self::default();
// attempt to construct settings with file
let from_file = Self::new_from_default(&default_settings);
match from_file {
Ok(f) => f,
Err(e) => {
warn!("Error reading config file ({:?})", e);
default_settings
}
}
}
fn new_from_default(default: &Settings) -> Result<Self, ConfigError> {
let builder = Config::builder();
let config: Config = builder
// use defaults
.add_source(Config::try_from(default)?)
// override with file contents
.add_source(File::with_name("config"))
.build()?
.try_into()
.unwrap();
let mut settings: Settings = config.try_deserialize()?;
// 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
);
}
// ensure durations parse
if !settings.verified_users.is_valid() {
panic!("VerifiedUsers time settings could not be parsed");
}
// initialize durations for verified users
settings.verified_users.init();
Ok(settings)
}
}
impl Default for Settings {
fn default() -> Self {
Settings {
info: Info {
relay_url: None,
name: Some("Unnamed nostr-rs-relay".to_owned()),
description: None,
pubkey: None,
contact: None,
},
database: Database {
data_directory: ".".to_owned(),
min_conn: 4,
max_conn: 128,
},
network: Network {
port: 8080,
address: "0.0.0.0".to_owned(),
},
limits: Limits {
messages_per_sec: None,
max_event_bytes: Some(2 << 17), // 128K
max_ws_message_bytes: Some(2 << 17), // 128K
max_ws_frame_bytes: Some(2 << 17), // 128K
broadcast_buffer: 16384,
event_persist_buffer: 4096,
},
authorization: Authorization {
pubkey_whitelist: None, // Allow any address to publish
},
verified_users: VerifiedUsers {
mode: VerifiedUsersMode::Disabled,
domain_whitelist: None,
domain_blacklist: None,
verify_expiration: Some("1 week".to_owned()),
verify_update_frequency: Some("1 day".to_owned()),
verify_expiration_duration: None,
verify_update_frequency_duration: None,
max_consecutive_failures: 20,
},
retention: Retention {
max_events: None, // max events
max_bytes: None, // max size
persist_days: None, // oldest message
whitelist_addresses: None, // whitelisted addresses (never delete)
},
options: Options {
reject_future_seconds: Some(30 * 60), // Reject events 30min in the future or greater
},
}
}
}
-102
View File
@@ -1,102 +0,0 @@
//! Client connection state
use crate::close::Close;
use crate::error::Error;
use crate::error::Result;
use crate::event::Event;
use crate::subscription::Subscription;
use log::*;
use std::collections::HashMap;
use uuid::Uuid;
/// A subscription identifier has a maximum length
const MAX_SUBSCRIPTION_ID_LEN: usize = 256;
/// State for a client connection
pub struct ClientConn {
/// Unique client identifier generated at connection time
client_id: Uuid,
/// The current set of active client subscriptions
subscriptions: HashMap<String, Subscription>,
/// Per-connection maximum concurrent subscriptions
max_subs: usize,
}
impl Default for ClientConn {
fn default() -> Self {
Self::new()
}
}
impl ClientConn {
/// Create a new, empty connection state.
pub fn new() -> Self {
let client_id = Uuid::new_v4();
ClientConn {
client_id,
subscriptions: HashMap::new(),
max_subs: 32,
}
}
/// Get a short prefix of the client's unique identifier, suitable
/// for logging.
pub fn get_client_prefix(&self) -> String {
self.client_id.to_string().chars().take(8).collect()
}
/// Find all matching subscriptions.
pub fn get_matching_subscriptions(&self, e: &Event) -> Vec<&str> {
let mut v: Vec<&str> = vec![];
for (id, sub) in self.subscriptions.iter() {
if sub.interested_in_event(e) {
v.push(id);
}
}
v
}
/// Add a new subscription for this connection.
pub fn subscribe(&mut self, s: Subscription) -> Result<()> {
let k = s.get_id();
let sub_id_len = k.len();
// prevent arbitrarily long subscription identifiers from
// being used.
if sub_id_len > MAX_SUBSCRIPTION_ID_LEN {
info!(
"ignoring sub request with excessive length: ({})",
sub_id_len
);
return Err(Error::SubIdMaxLengthError);
}
// check if an existing subscription exists, and replace if so
if self.subscriptions.contains_key(&k) {
self.subscriptions.remove(&k);
self.subscriptions.insert(k, s);
debug!("replaced existing subscription");
return Ok(());
}
// check if there is room for another subscription.
if self.subscriptions.len() >= self.max_subs {
return Err(Error::SubMaxExceededError);
}
// add subscription
self.subscriptions.insert(k, s);
debug!(
"registered new subscription, currently have {} active subs",
self.subscriptions.len()
);
Ok(())
}
/// Remove the subscription for this connection.
pub fn unsubscribe(&mut self, c: Close) {
// TODO: return notice if subscription did not exist.
self.subscriptions.remove(&c.id);
debug!(
"removed subscription, currently have {} active subs",
self.subscriptions.len()
);
}
}
-589
View File
@@ -1,589 +0,0 @@
//! Event persistence and querying
use crate::config::SETTINGS;
use crate::error::Error;
use crate::error::Result;
use crate::event::Event;
use crate::hexrange::hex_range;
use crate::hexrange::HexSearch;
use crate::nip05;
use crate::schema::{upgrade_db, STARTUP_SQL};
use crate::subscription::Subscription;
use crate::utils::is_hex;
use governor::clock::Clock;
use governor::{Quota, RateLimiter};
use hex;
use log::*;
use r2d2;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::params;
use rusqlite::types::ToSql;
use rusqlite::Connection;
use rusqlite::OpenFlags;
use std::path::Path;
use std::thread;
use std::time::Duration;
use std::time::Instant;
use tokio::task;
pub type SqlitePool = r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>;
pub type PooledConnection = r2d2::PooledConnection<r2d2_sqlite::SqliteConnectionManager>;
/// Events submitted from a client, with a return channel for notices
pub struct SubmittedEvent {
pub event: Event,
pub notice_tx: tokio::sync::mpsc::Sender<String>,
}
/// Database file
pub const DB_FILE: &str = "nostr.db";
/// Build a database connection pool.
pub fn build_pool(
name: &str,
flags: OpenFlags,
min_size: u32,
max_size: u32,
wait_for_db: bool,
) -> SqlitePool {
let settings = SETTINGS.read().unwrap();
let db_dir = &settings.database.data_directory;
let full_path = Path::new(db_dir).join(DB_FILE);
// small hack; if the database doesn't exist yet, that means the
// writer thread hasn't finished. Give it a chance to work. This
// is only an issue with the first time we run.
while !full_path.exists() && wait_for_db {
debug!("Database reader pool is waiting on the database to be created...");
thread::sleep(Duration::from_millis(500));
}
let manager = SqliteConnectionManager::file(&full_path)
.with_flags(flags)
.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(min_size))
.max_size(max_size)
.build(manager)
.unwrap();
info!(
"Built a connection pool {:?} (min={}, max={})",
name, min_size, max_size
);
pool
}
/// Build a single database connection, with provided flags
pub fn build_conn(flags: OpenFlags) -> Result<Connection> {
let settings = SETTINGS.read().unwrap();
let db_dir = &settings.database.data_directory;
let full_path = Path::new(db_dir).join(DB_FILE);
// create a connection
Ok(Connection::open_with_flags(&full_path, flags)?)
}
/// Spawn a database writer that persists events to the SQLite store.
pub async fn db_writer(
mut event_rx: tokio::sync::mpsc::Receiver<SubmittedEvent>,
bcast_tx: tokio::sync::broadcast::Sender<Event>,
metadata_tx: tokio::sync::broadcast::Sender<Event>,
mut shutdown: tokio::sync::broadcast::Receiver<()>,
) -> tokio::task::JoinHandle<Result<()>> {
let settings = SETTINGS.read().unwrap();
// are we performing NIP-05 checking?
let nip05_active = settings.verified_users.is_active();
// are we requriing NIP-05 user verification?
let nip05_enabled = settings.verified_users.is_enabled();
task::spawn_blocking(move || {
// get database configuration settings
let settings = SETTINGS.read().unwrap();
let db_dir = &settings.database.data_directory;
let full_path = Path::new(db_dir).join(DB_FILE);
// create a connection pool
let pool = build_pool(
"event writer",
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
1,
4,
false,
);
info!("opened database {:?} for writing", full_path);
upgrade_db(&mut pool.get()?)?;
// Make a copy of the whitelist
let whitelist = &settings.authorization.pubkey_whitelist.clone();
// get rate limit settings
let rps_setting = settings.limits.messages_per_sec;
let mut most_recent_rate_limit = Instant::now();
let mut lim_opt = None;
let clock = governor::clock::QuantaClock::default();
if let Some(rps) = rps_setting {
if rps > 0 {
info!("Enabling rate limits for event creation ({}/sec)", rps);
let quota = core::num::NonZeroU32::new(rps * 60).unwrap();
lim_opt = Some(RateLimiter::direct(Quota::per_minute(quota)));
}
}
loop {
if shutdown.try_recv().is_ok() {
info!("shutting down database writer");
break;
}
// call blocking read on channel
let next_event = event_rx.blocking_recv();
// if the channel has closed, we will never get work
if next_event.is_none() {
break;
}
let mut event_write = false;
let subm_event = next_event.unwrap();
let event = subm_event.event;
let notice_tx = subm_event.notice_tx;
// check if this event is authorized.
if let Some(allowed_addrs) = whitelist {
// if the event address is not in allowed_addrs.
if !allowed_addrs.contains(&event.pubkey) {
info!(
"Rejecting event {}, unauthorized author",
event.get_event_id_prefix()
);
notice_tx
.try_send("pubkey is not allowed to publish to this relay".to_owned())
.ok();
continue;
}
}
// send any metadata events to the NIP-05 verifier
if nip05_active && event.is_kind_metadata() {
// we are sending this prior to even deciding if we
// persist it. this allows the nip05 module to
// inspect it, update if necessary, or persist a new
// event and broadcast it itself.
metadata_tx.send(event.clone()).ok();
}
// check for NIP-05 verification
if nip05_enabled {
match nip05::query_latest_user_verification(pool.get()?, event.pubkey.to_owned()) {
Ok(uv) => {
if uv.is_valid() {
info!(
"new event from verified author ({:?},{:?})",
uv.name.to_string(),
event.get_author_prefix()
);
} else {
info!("rejecting event, author ({:?} / {:?}) verification invalid (expired/wrong domain)",
uv.name.to_string(),
event.get_author_prefix()
);
notice_tx
.try_send(
"NIP-05 verification is no longer valid (expired/wrong domain)"
.to_owned(),
)
.ok();
continue;
}
}
Err(Error::SqlError(rusqlite::Error::QueryReturnedNoRows)) => {
debug!(
"no verification records found for pubkey: {:?}",
event.get_author_prefix()
);
notice_tx
.try_send("NIP-05 verification needed to publish events".to_owned())
.ok();
continue;
}
Err(e) => {
warn!("checking nip05 verification status failed: {:?}", e);
continue;
}
}
}
// TODO: cache recent list of authors to remove a DB call.
let start = Instant::now();
match write_event(&mut pool.get()?, &event) {
Ok(updated) => {
if updated == 0 {
trace!("ignoring duplicate event");
} else {
info!(
"persisted event {:?} from {:?} in {:?}",
event.get_event_id_prefix(),
event.get_author_prefix(),
start.elapsed()
);
event_write = true;
// send this out to all clients
bcast_tx.send(event.clone()).ok();
}
}
Err(err) => {
warn!("event insert failed: {:?}", err);
notice_tx
.try_send(
"relay experienced an error trying to publish the latest event"
.to_owned(),
)
.ok();
}
}
// use rate limit, if defined, and if an event was actually written.
if event_write {
if let Some(ref lim) = lim_opt {
if let Err(n) = lim.check() {
let wait_for = n.wait_time_from(clock.now());
// check if we have recently logged rate
// limits, but print out a message only once
// per second.
if most_recent_rate_limit.elapsed().as_secs() > 10 {
warn!(
"rate limit reached for event creation (sleep for {:?}) (suppressing future messages for 10 seconds)",
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;
}
}
}
}
info!("database connection closed");
Ok(())
})
}
/// Persist an event to the database, returning rows added.
pub fn write_event(conn: &mut PooledConnection, e: &Event) -> Result<usize> {
// start transaction
let tx = conn.transaction()?;
// get relevant fields from event and convert to blobs.
let id_blob = hex::decode(&e.id).ok();
let pubkey_blob = hex::decode(&e.pubkey).ok();
let event_str = serde_json::to_string(&e).ok();
// ignore if the event hash is a duplicate.
let ins_count = tx.execute(
"INSERT OR IGNORE INTO event (event_hash, created_at, kind, author, content, first_seen, hidden) VALUES (?1, ?2, ?3, ?4, ?5, strftime('%s','now'), FALSE);",
params![id_blob, e.created_at, e.kind, pubkey_blob, event_str]
)?;
if ins_count == 0 {
// if the event was a duplicate, no need to insert event or
// pubkey references.
return Ok(ins_count);
}
// remember primary key of the event most recently inserted.
let ev_id = tx.last_insert_rowid();
// add all tags to the tag table
for tag in e.tags.iter() {
// ensure we have 2 values.
if tag.len() >= 2 {
let tagname = &tag[0];
let tagval = &tag[1];
// if tagvalue is hex;
if is_hex(tagval) {
tx.execute(
"INSERT OR IGNORE INTO tag (event_id, name, value_hex) VALUES (?1, ?2, ?3)",
params![ev_id, &tagname, hex::decode(&tagval).ok()],
)?;
} else {
tx.execute(
"INSERT OR IGNORE INTO tag (event_id, name, value) VALUES (?1, ?2, ?3)",
params![ev_id, &tagname, &tagval],
)?;
}
}
}
// if this event is for a metadata update, hide every other kind=0
// event from the same author that was issued earlier than this.
if e.kind == 0 {
let update_count = tx.execute(
"UPDATE event SET hidden=TRUE WHERE id!=? AND kind=0 AND author=? AND created_at <= ? and hidden!=TRUE",
params![ev_id, hex::decode(&e.pubkey).ok(), e.created_at],
)?;
if update_count > 0 {
info!(
"hid {} older metadata events for author {:?}",
update_count,
e.get_author_prefix()
);
}
}
// if this event is for a contact update, hide every other kind=3
// event from the same author that was issued earlier than this.
if e.kind == 3 {
let update_count = tx.execute(
"UPDATE event SET hidden=TRUE WHERE id!=? AND kind=3 AND author=? AND created_at <= ? and hidden!=TRUE",
params![ev_id, hex::decode(&e.pubkey).ok(), e.created_at],
)?;
if update_count > 0 {
info!(
"hid {} older contact events for author {:?}",
update_count,
e.get_author_prefix()
);
}
}
// if this event is a deletion, hide the referenced events from the same author.
if e.kind == 5 {
let event_candidates = e.tag_values_by_name("e");
let mut params: Vec<Box<dyn ToSql>> = vec![];
// first parameter will be author
params.push(Box::new(hex::decode(&e.pubkey)?));
event_candidates
.iter()
.filter(|x| is_hex(x) && x.len() == 64)
.filter_map(|x| hex::decode(x).ok())
.for_each(|x| params.push(Box::new(x)));
let query = format!(
"UPDATE event SET hidden=TRUE WHERE author=? AND event_hash IN ({})",
repeat_vars(params.len() - 1)
);
let mut stmt = tx.prepare(&query)?;
let update_count = stmt.execute(rusqlite::params_from_iter(params))?;
info!(
"hid {} deleted events for author {:?}",
update_count,
e.get_author_prefix()
);
}
tx.commit()?;
Ok(ins_count)
}
/// Serialized event associated with a specific subscription request.
#[derive(PartialEq, Debug, Clone)]
pub struct QueryResult {
/// Subscription identifier
pub sub_id: String,
/// Serialized event
pub event: String,
}
/// Produce a arbitrary list of '?' parameters.
fn repeat_vars(count: usize) -> String {
if count == 0 {
return "".to_owned();
}
let mut s = "?,".repeat(count);
// Remove trailing comma
s.pop();
s
}
/// Create a dynamic SQL query string and params from a subscription.
fn query_from_sub(sub: &Subscription) -> (String, Vec<Box<dyn ToSql>>) {
// build a dynamic SQL query. all user-input is either an integer
// (sqli-safe), or a string that is filtered to only contain
// hexadecimal characters. Strings that require escaping (tag
// names/values) use parameters.
let mut limit: Option<u32> = None;
let mut query =
"SELECT DISTINCT(e.content) FROM event e LEFT JOIN tag t ON e.id=t.event_id ".to_owned();
// parameters
let mut params: Vec<Box<dyn ToSql>> = vec![];
// for every filter in the subscription, generate a where clause
let mut filter_clauses: Vec<String> = Vec::new();
for f in sub.filters.iter() {
// individual filter components
let mut filter_components: Vec<String> = Vec::new();
// Query for "authors", allowing prefix matches
if let Some(authvec) = &f.authors {
// take each author and convert to a hexsearch
let mut auth_searches: Vec<String> = vec![];
for auth in authvec {
match hex_range(auth) {
Some(HexSearch::Exact(ex)) => {
auth_searches.push("author=?".to_owned());
params.push(Box::new(ex));
}
Some(HexSearch::Range(lower, upper)) => {
auth_searches.push("(author>? AND author<?)".to_owned());
params.push(Box::new(lower));
params.push(Box::new(upper));
}
Some(HexSearch::LowerOnly(lower)) => {
auth_searches.push("author>?".to_owned());
params.push(Box::new(lower));
}
None => {
info!("Could not parse hex range from author {:?}", auth);
}
}
}
let authors_clause = format!("({})", auth_searches.join(" OR "));
filter_components.push(authors_clause);
}
if let Some(lim) = f.limit {
limit = Some(lim)
}
// Query for Kind
if let Some(ks) = &f.kinds {
// kind is number, no escaping needed
let str_kinds: Vec<String> = ks.iter().map(|x| x.to_string()).collect();
let kind_clause = format!("kind IN ({})", str_kinds.join(", "));
filter_components.push(kind_clause);
}
// Query for event, allowing prefix matches
if let Some(idvec) = &f.ids {
// take each author and convert to a hexsearch
let mut id_searches: Vec<String> = vec![];
for id in idvec {
match hex_range(id) {
Some(HexSearch::Exact(ex)) => {
id_searches.push("event_hash=?".to_owned());
params.push(Box::new(ex));
}
Some(HexSearch::Range(lower, upper)) => {
id_searches.push("(event_hash>? AND event_hash<?)".to_owned());
params.push(Box::new(lower));
params.push(Box::new(upper));
}
Some(HexSearch::LowerOnly(lower)) => {
id_searches.push("event_hash>?".to_owned());
params.push(Box::new(lower));
}
None => {
info!("Could not parse hex range from id {:?}", id);
}
}
}
let id_clause = format!("({})", id_searches.join(" OR "));
filter_components.push(id_clause);
}
// Query for tags
if let Some(map) = &f.tags {
for (key, val) in map.iter() {
let mut str_vals: Vec<Box<dyn ToSql>> = vec![];
let mut blob_vals: Vec<Box<dyn ToSql>> = vec![];
for v in val {
if is_hex(v) {
if let Ok(h) = hex::decode(&v) {
blob_vals.push(Box::new(h));
}
} else {
str_vals.push(Box::new(v.to_owned()));
}
}
// create clauses with "?" params for each tag value being searched
let str_clause = format!("value IN ({})", repeat_vars(str_vals.len()));
let blob_clause = format!("value_hex IN ({})", repeat_vars(blob_vals.len()));
let tag_clause = format!("(name=? AND ({} OR {}))", str_clause, blob_clause);
// add the tag name as the first parameter
params.push(Box::new(key.to_owned()));
// add all tag values that are plain strings as params
params.append(&mut str_vals);
// add all tag values that are blobs as params
params.append(&mut blob_vals);
filter_components.push(tag_clause);
}
}
// Query for timestamp
if f.since.is_some() {
let created_clause = format!("created_at > {}", f.since.unwrap());
filter_components.push(created_clause);
}
// Query for timestamp
if f.until.is_some() {
let until_clause = format!("created_at < {}", f.until.unwrap());
filter_components.push(until_clause);
}
// combine all clauses, and add to filter_clauses
if !filter_components.is_empty() {
let mut fc = "( ".to_owned();
fc.push_str(&filter_components.join(" AND "));
fc.push_str(" )");
filter_clauses.push(fc);
}
}
// never display hidden events
query.push_str(" WHERE hidden!=TRUE ");
// combine all filters with OR clauses, if any exist
if !filter_clauses.is_empty() {
query.push_str(" AND (");
query.push_str(&filter_clauses.join(" OR "));
query.push_str(") ");
}
// add order clause
query.push_str(&format!(
" ORDER BY created_at {}",
limit.map_or("ASC", |_| "DESC")
));
if let Some(lim) = limit {
query.push_str(&format!(" LIMIT {}", lim))
}
debug!("query string: {}", query);
(query, params)
}
/// Perform a database query using a subscription.
///
/// The [`Subscription`] is converted into a SQL query. Each result
/// is published on the `query_tx` channel as it is returned. If a
/// message becomes available on the `abandon_query_rx` channel, the
/// query is immediately aborted.
pub async fn db_query(
sub: Subscription,
pool: SqlitePool,
query_tx: tokio::sync::mpsc::Sender<QueryResult>,
mut abandon_query_rx: tokio::sync::oneshot::Receiver<()>,
) {
task::spawn_blocking(move || {
debug!("going to query for: {:?}", sub);
let mut row_count: usize = 0;
let start = Instant::now();
// generate SQL query
let (q, p) = query_from_sub(&sub);
debug!("SQL generated in {:?}", start.elapsed());
// show pool stats
debug!("DB pool stats: {:?}", pool.state());
let start = Instant::now();
if let Ok(conn) = pool.get() {
// execute the query. Don't cache, since queries vary so much.
let mut stmt = conn.prepare(&q)?;
let mut event_rows = stmt.query(rusqlite::params_from_iter(p))?;
let mut first_result = true;
while let Some(row) = event_rows.next()? {
if first_result {
debug!("time to first result: {:?}", start.elapsed());
first_result = false;
}
// check if this is still active
// TODO: check every N rows
if abandon_query_rx.try_recv().is_ok() {
debug!("query aborted");
return Ok(());
}
row_count += 1;
let event_json = row.get(0)?;
query_tx
.blocking_send(QueryResult {
sub_id: sub.get_id(),
event: event_json,
})
.ok();
}
debug!(
"query completed ({} rows) in {:?}",
row_count,
start.elapsed()
);
} else {
warn!("Could not get a database connection for querying");
}
let ok: Result<()> = Ok(());
ok
});
}
+8 -96
View File
@@ -1,114 +1,26 @@
//! Error handling
//! Error handling.
use std::result;
use thiserror::Error;
use tungstenite::error::Error as WsError;
/// Simple `Result` type for errors in this module
pub type Result<T, E = Error> = result::Result<T, E>;
/// Custom error type for Nostr
#[derive(Error, Debug)]
pub enum Error {
#[error("Protocol parse error")]
ProtoParseError,
#[error("Connection error")]
ConnError,
#[error("Client write error")]
ConnWriteError,
#[error("EVENT parse failed")]
#[error("command from client not recognized")]
CommandNotFound,
#[error("parsing JSON->Event failed")]
EventParseFailed,
#[error("ClOSE message parse failed")]
#[error("parsing JSON->Req failed")]
ReqParseFailed,
#[error("parsing JSON->Close failed")]
CloseParseFailed,
#[error("Event validation failed")]
EventInvalid,
#[error("Event too large")]
EventMaxLengthError(usize),
#[error("Subscription identifier max length exceeded")]
SubIdMaxLengthError,
#[error("Maximum concurrent subscription count reached")]
SubMaxExceededError,
// this should be used if the JSON is invalid
#[error("JSON parsing failed")]
JsonParseFailed(serde_json::Error),
#[error("WebSocket proto error")]
WebsocketError(WsError),
#[error("Command unknown")]
CommandUnknownError,
#[error("SQL error")]
SqlError(rusqlite::Error),
#[error("Config error")]
ConfigError(config::ConfigError),
#[error("Data directory does not exist")]
DatabaseDirError,
#[error("Database Connection Pool Error")]
DatabasePoolError(r2d2::Error),
#[error("Custom Error : {0}")]
CustomError(String),
#[error("Task join error")]
JoinError,
#[error("Hyper Client error")]
HyperError(hyper::Error),
#[error("Hex encoding error")]
HexError(hex::FromHexError),
#[error("Unknown/Undocumented")]
UnknownError,
}
//impl From<Box<dyn std::error::Error>> for Error {
// fn from(e: Box<dyn std::error::Error>) -> Self {
// Error::CustomError("error".to_owned())
// }
//}
impl From<hex::FromHexError> for Error {
fn from(h: hex::FromHexError) -> Self {
Error::HexError(h)
}
}
impl From<hyper::Error> for Error {
fn from(h: hyper::Error) -> Self {
Error::HyperError(h)
}
}
impl From<r2d2::Error> for Error {
fn from(d: r2d2::Error) -> Self {
Error::DatabasePoolError(d)
}
}
impl From<tokio::task::JoinError> for Error {
/// Wrap SQL error
fn from(_j: tokio::task::JoinError) -> Self {
Error::JoinError
}
}
impl From<rusqlite::Error> for Error {
/// Wrap SQL error
fn from(r: rusqlite::Error) -> Self {
Error::SqlError(r)
}
}
impl From<serde_json::Error> for Error {
/// Wrap JSON error
fn from(r: serde_json::Error) -> Self {
Error::JsonParseFailed(r)
}
}
impl From<WsError> for Error {
/// Wrap Websocket error
fn from(r: WsError) -> Self {
Error::WebsocketError(r)
}
}
impl From<config::ConfigError> for Error {
/// Wrap Config error
fn from(r: config::ConfigError) -> Self {
Error::ConfigError(r)
}
}
+31 -300
View File
@@ -1,242 +1,57 @@
//! Event parsing and validation
use crate::config;
use crate::error::Error::*;
use crate::error::Result;
use crate::nip05;
use crate::utils::unix_time;
use bitcoin_hashes::{sha256, Hash};
use lazy_static::lazy_static;
use log::*;
use secp256k1::{schnorr, Secp256k1, VerifyOnly, XOnlyPublicKey};
use crate::error::{Error, Result};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::value::Value;
use serde_json::Number;
use std::collections::HashMap;
use std::collections::HashSet;
use std::str::FromStr;
//use serde_json::json;
//use serde_json::Result;
lazy_static! {
/// Secp256k1 verification instance.
pub static ref SECP: Secp256k1<VerifyOnly> = Secp256k1::verification_only();
}
/// Event command in network format.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct EventCmd {
cmd: String, // expecting static "EVENT"
event: Event,
}
/// Parsed nostr event.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Event {
pub id: String,
pub(crate) id: String,
pub(crate) pubkey: String,
pub(crate) created_at: u64,
pub(crate) kind: u64,
pub(crate) kind: u8,
#[serde(deserialize_with = "tag_from_string")]
// NOTE: array-of-arrays may need to be more general than a string container
pub(crate) tags: Vec<Vec<String>>,
pub(crate) content: String,
pub(crate) sig: String,
// Optimization for tag search, built on demand
#[serde(skip)]
pub(crate) tagidx: Option<HashMap<String, HashSet<String>>>,
}
/// Simple tag type for array of array of strings.
type Tag = Vec<Vec<String>>;
/// Deserializer that ensures we always have a [`Tag`].
// handle a default value (empty vec) for null tags
fn tag_from_string<'de, D>(deserializer: D) -> Result<Tag, D::Error>
where
D: Deserializer<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_else(Vec::new))
Ok(opt.unwrap_or_else(|| vec![]))
}
/// Convert network event to parsed/validated event.
impl From<EventCmd> for Result<Event> {
fn from(ec: EventCmd) -> Result<Event> {
// ensure command is correct
if ec.cmd != "EVENT" {
Err(CommandUnknownError)
} else if ec.event.is_valid() {
let mut e = ec.event;
e.build_index();
Ok(e)
} else {
Err(EventInvalid)
}
}
}
// Goals:
// Roundtrip from JSON-string to Event, and back to string.
// Perform validation on an Event to ensure the id and signature are correct.
impl Event {
pub fn is_kind_metadata(&self) -> bool {
self.kind == 0
pub fn parse(json: &str) -> Result<Event> {
let _e: Event = serde_json::from_str(json)?;
Err(Error::EventParseFailed)
}
/// Pull a NIP-05 Name out of the event, if one exists
pub fn get_nip05_addr(&self) -> Option<nip05::Nip05Name> {
if self.is_kind_metadata() {
// very quick check if we should attempt to parse this json
if self.content.contains("\"nip05\"") {
// Parse into JSON
let md_parsed: Value = serde_json::from_str(&self.content).ok()?;
let md_map = md_parsed.as_object()?;
let nip05_str = md_map.get("nip05")?.as_str()?;
return nip05::Nip05Name::try_from(nip05_str).ok();
}
}
None
}
/// Build an event tag index
fn build_index(&mut self) {
// if there are no tags; just leave the index as None
if self.tags.is_empty() {
return;
}
// otherwise, build an index
let mut idx: HashMap<String, HashSet<String>> = HashMap::new();
// iterate over tags that have at least 2 elements
for t in self.tags.iter().filter(|x| x.len() > 1) {
let tagname = t.get(0).unwrap();
let tagval = t.get(1).unwrap();
// ensure a vector exists for this tag
if !idx.contains_key(tagname) {
idx.insert(tagname.clone(), HashSet::new());
}
// get the tag vec and insert entry
let tidx = idx.get_mut(tagname).expect("could not get tag vector");
tidx.insert(tagval.clone());
}
// save the tag structure
self.tagidx = Some(idx);
}
/// Create a short event identifier, suitable for logging.
pub fn get_event_id_prefix(&self) -> String {
self.id.chars().take(8).collect()
}
pub fn get_author_prefix(&self) -> String {
self.pubkey.chars().take(8).collect()
}
/// Retrieve tag values
pub fn tag_values_by_name(&self, tag_name: &str) -> Vec<String> {
self.tags
.iter()
.filter(|x| x.len() > 1)
.filter(|x| x.get(0).unwrap() == tag_name)
.map(|x| x.get(1).unwrap().to_owned())
.collect()
}
/// Check if this event has a valid signature.
fn is_valid(&self) -> bool {
// TODO: return a Result with a reason for invalid events
// don't bother to validate an event with a timestamp in the distant future.
let config = config::SETTINGS.read().unwrap();
let max_future_sec = config.options.reject_future_seconds;
if let Some(allowable_future) = max_future_sec {
let curr_time = unix_time();
// calculate difference, plus how far future we allow
if curr_time + (allowable_future as u64) < self.created_at {
let delta = self.created_at - curr_time;
debug!(
"event is too far in the future ({} seconds), rejecting",
delta
);
return false;
}
}
// validation is performed by:
// * parsing JSON string into event fields
// * create an array:
// ** [0, pubkey-hex-string, created-at-num, kind-num, tags-array-of-arrays, content-string]
// * serialize with no spaces/newlines
let c_opt = self.to_canonical();
if c_opt.is_none() {
debug!("event could not be canonicalized");
return false;
}
let c = c_opt.unwrap();
// * compute the sha256sum.
let digest: sha256::Hash = sha256::Hash::hash(c.as_bytes());
let hex_digest = format!("{:x}", digest);
// * ensure the id matches the computed sha256sum.
if self.id != hex_digest {
debug!("event id does not match digest");
return false;
}
// * validate the message digest (sig) using the pubkey & computed sha256 message hash.
let sig = schnorr::Signature::from_str(&self.sig).unwrap();
if let Ok(msg) = secp256k1::Message::from_slice(digest.as_ref()) {
if let Ok(pubkey) = XOnlyPublicKey::from_str(&self.pubkey) {
let verify = SECP.verify_schnorr(&sig, &msg, &pubkey);
matches!(verify, Ok(()))
} else {
debug!("client sent malformed pubkey");
// check if this event is valid (should be propagated, stored) based on signature.
pub fn is_valid(&self) -> bool {
false
}
} else {
info!("error converting digest to secp256k1 message");
false
}
}
/// Convert event to canonical representation for signing.
fn to_canonical(&self) -> Option<String> {
// create a JsonValue for each event element
let mut c: Vec<Value> = vec![];
// id must be set to 0
let id = Number::from(0_u64);
c.push(serde_json::Value::Number(id));
// public key
c.push(Value::String(self.pubkey.to_owned()));
// creation time
let created_at = Number::from(self.created_at);
c.push(serde_json::Value::Number(created_at));
// kind
let kind = Number::from(self.kind);
c.push(serde_json::Value::Number(kind));
// tags
c.push(self.tags_to_canonical());
// content
c.push(Value::String(self.content.to_owned()));
serde_json::to_string(&Value::Array(c)).ok()
}
/// Convert tags to a canonical form for signing.
fn tags_to_canonical(&self) -> Value {
let mut tags = Vec::<Value>::new();
// iterate over self tags,
// check if given event is referenced in a tag
pub fn event_tag_match(&self, event: &str) -> bool {
for t in self.tags.iter() {
// each tag is a vec of strings
let mut a = Vec::<Value>::new();
for v in t.iter() {
a.push(serde_json::Value::String(v.to_owned()));
if t.len() == 2 {
if t.get(0).unwrap() == "#e" {
if t.get(1).unwrap() == event {
return true;
}
tags.push(serde_json::Value::Array(a));
}
serde_json::Value::Array(tags)
}
/// Determine if the given tag and value set intersect with tags in this event.
pub fn generic_tag_val_intersect(&self, tagname: &str, check: &HashSet<String>) -> bool {
match &self.tagidx {
Some(idx) => match idx.get(tagname) {
Some(valset) => {
let common = valset.intersection(check);
common.count() > 0
}
None => false,
},
None => false,
}
return false;
}
}
@@ -244,7 +59,7 @@ impl Event {
mod tests {
use super::*;
fn simple_event() -> Event {
Event {
super::Event {
id: "0".to_owned(),
pubkey: "0".to_owned(),
created_at: 0,
@@ -252,7 +67,6 @@ mod tests {
tags: vec![],
content: "".to_owned(),
sig: "0".to_owned(),
tagidx: None,
}
}
@@ -272,29 +86,6 @@ mod tests {
Ok(())
}
#[test]
fn empty_event_tag_match() -> Result<()> {
let event = simple_event();
assert!(!event
.generic_tag_val_intersect("e", &HashSet::from(["foo".to_owned(), "bar".to_owned()])));
Ok(())
}
#[test]
fn single_event_tag_match() -> Result<()> {
let mut event = simple_event();
event.tags = vec![vec!["e".to_owned(), "foo".to_owned()]];
event.build_index();
assert_eq!(
event.generic_tag_val_intersect(
"e",
&HashSet::from(["foo".to_owned(), "bar".to_owned()])
),
true
);
Ok(())
}
#[test]
fn event_tags_serialize() -> Result<()> {
// serialize an event with tags to JSON string
@@ -319,77 +110,17 @@ mod tests {
#[test]
fn event_deserialize() -> Result<()> {
let raw_json = r#"{"id":"1384757da583e6129ce831c3d7afc775a33a090578f888dd0d010328ad047d0c","pubkey":"bbbd9711d357df4f4e498841fd796535c95c8e751fa35355008a911c41265fca","created_at":1612650459,"kind":1,"tags":null,"content":"hello world","sig":"59d0cc47ab566e81f72fe5f430bcfb9b3c688cb0093d1e6daa49201c00d28ecc3651468b7938642869ed98c0f1b262998e49a05a6ed056c0d92b193f4e93bc21"}"#;
// id: 1384757da583e6129ce831c3d7afc775a33a090578f888dd0d010328ad047d0c
// pubkey: bbbd9711d357df4f4e498841fd796535c95c8e751fa35355008a911c41265fca",
// created_at: 1612650459
// kind :1,
// tags":null,
// "content":"hello world",
// "sig":"59d0cc47ab566e81f72fe5f430bcfb9b3c688cb0093d1e6daa49201c00d28ecc3651468b7938642869ed98c0f1b262998e49a05a6ed056c0d92b193f4e93bc21"}]"#;
let e: Event = serde_json::from_str(raw_json)?;
// assert that the kind is 1
assert_eq!(e.kind, 1);
assert_eq!(e.tags.len(), 0);
Ok(())
}
#[test]
fn event_canonical() {
let e = Event {
id: "999".to_owned(),
pubkey: "012345".to_owned(),
created_at: 501234,
kind: 1,
tags: vec![],
content: "this is a test".to_owned(),
sig: "abcde".to_owned(),
tagidx: None,
};
let c = e.to_canonical();
let expected = Some(r#"[0,"012345",501234,1,[],"this is a test"]"#.to_owned());
assert_eq!(c, expected);
}
#[test]
fn event_tag_select() {
let e = Event {
id: "999".to_owned(),
pubkey: "012345".to_owned(),
created_at: 501234,
kind: 1,
tags: vec![
vec!["j".to_owned(), "abc".to_owned()],
vec!["e".to_owned(), "foo".to_owned()],
vec!["e".to_owned(), "bar".to_owned()],
vec!["e".to_owned(), "baz".to_owned()],
vec![
"p".to_owned(),
"aaaa".to_owned(),
"ws://example.com".to_owned(),
],
],
content: "this is a test".to_owned(),
sig: "abcde".to_owned(),
tagidx: None,
};
let v = e.tag_values_by_name("e");
assert_eq!(v, vec!["foo", "bar", "baz"]);
}
#[test]
fn event_canonical_with_tags() {
let e = Event {
id: "999".to_owned(),
pubkey: "012345".to_owned(),
created_at: 501234,
kind: 1,
tags: vec![
vec!["#e".to_owned(), "aoeu".to_owned()],
vec![
"#p".to_owned(),
"aaaa".to_owned(),
"ws://example.com".to_owned(),
],
],
content: "this is a test".to_owned(),
sig: "abcde".to_owned(),
tagidx: None,
};
let c = e.to_canonical();
let expected_json = r###"[0,"012345",501234,1,[["#e","aoeu"],["#p","aaaa","ws://example.com"]],"this is a test"]"###;
let expected = Some(expected_json.to_owned());
assert_eq!(c, expected);
}
}
-159
View File
@@ -1,159 +0,0 @@
//! Utilities for searching hexadecimal
use crate::utils::is_hex;
use hex;
/// Types of hexadecimal queries.
#[derive(PartialEq, Debug, Clone)]
pub 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>),
}
/// Check if a string contains only f chars
fn is_all_fs(s: &str) -> bool {
s.chars().all(|x| x == 'f' || x == 'F')
}
/// Find the next hex sequence greater than the argument.
pub 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))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Result;
#[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 = "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(())
}
}
-43
View File
@@ -1,43 +0,0 @@
//! Relay metadata using NIP-11
/// Relay Info
use crate::config;
use serde::{Deserialize, Serialize};
pub 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()),
}
}
}
+1 -8
View File
@@ -1,12 +1,5 @@
pub mod close;
pub mod config;
pub mod conn;
pub mod db;
pub mod error;
pub mod event;
pub mod hexrange;
pub mod info;
pub mod nip05;
pub mod schema;
pub mod proto;
pub mod subscription;
pub mod utils;
+44 -561
View File
@@ -1,583 +1,66 @@
//! Server process
use futures::SinkExt;
use futures::StreamExt;
use hyper::header::ACCEPT;
use hyper::service::{make_service_fn, service_fn};
use hyper::upgrade::Upgraded;
use hyper::{
header, server::conn::AddrStream, upgrade, Body, Request, Response, Server, StatusCode,
};
use log::*;
use nostr_rs_relay::close::Close;
use nostr_rs_relay::close::CloseCmd;
use nostr_rs_relay::config;
use nostr_rs_relay::conn;
use nostr_rs_relay::db;
use nostr_rs_relay::db::SubmittedEvent;
use nostr_rs_relay::error::{Error, Result};
use nostr_rs_relay::event::Event;
use nostr_rs_relay::event::EventCmd;
use nostr_rs_relay::info::RelayInfo;
use nostr_rs_relay::nip05;
use nostr_rs_relay::subscription::Subscription;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;
use std::convert::Infallible;
use std::env;
use std::net::SocketAddr;
use std::path::Path;
use std::time::Duration;
use std::time::Instant;
use log::info;
use nostr_rs_relay::proto::Proto;
use std::{env, io::Error};
use tokio::net::{TcpListener, TcpStream};
use tokio::runtime::Builder;
use tokio::sync::broadcast::{self, Receiver, Sender};
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio_tungstenite::WebSocketStream;
use tungstenite::error::Error as WsError;
use tungstenite::handshake;
use tungstenite::protocol::Message;
use tungstenite::protocol::WebSocketConfig;
/// Return a requested DB name from command line arguments.
fn db_from_args(args: Vec<String>) -> Option<String> {
if args.len() == 3 && args.get(1) == Some(&"--db".to_owned()) {
return args.get(2).map(|x| x.to_owned());
}
None
}
/// Handle arbitrary HTTP requests, including for WebSocket upgrades.
async fn handle_web_request(
mut request: Request<Body>,
pool: db::SqlitePool,
remote_addr: SocketAddr,
broadcast: Sender<Event>,
event_tx: tokio::sync::mpsc::Sender<SubmittedEvent>,
shutdown: Receiver<()>,
) -> Result<Response<Body>, Infallible> {
match (
request.uri().path(),
request.headers().contains_key(header::UPGRADE),
) {
// Request for / as websocket
("/", true) => {
trace!("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.
fn main() -> Result<(), Error> {
// setup logger
let _ = env_logger::try_init();
// get database directory from args
let args: Vec<String> = env::args().collect();
let db_dir: Option<String> = db_from_args(args);
{
let mut settings = config::SETTINGS.write().unwrap();
// replace default settings with those read from config.toml
let mut c = config::Settings::new();
// update with database location
if let Some(db) = db_dir {
c.database.data_directory = db;
}
*settings = c;
}
let settings = config::SETTINGS.read().unwrap();
trace!("Config: {:?}", settings);
// do some config validation.
if !Path::new(&settings.database.data_directory).is_dir() {
error!("Database directory does not exist");
return Err(Error::DatabaseDirError);
}
let addr = format!(
"{}:{}",
settings.network.address.trim(),
settings.network.port
);
let socket_addr = addr.parse().expect("listening address not valid");
// address whitelisting settings
if let Some(addr_whitelist) = &settings.authorization.pubkey_whitelist {
info!(
"Event publishing restricted to {} pubkey(s)",
addr_whitelist.len()
);
}
// check if NIP-05 enforced user verification is on
if settings.verified_users.is_active() {
info!(
"NIP-05 user verification mode:{:?}",
settings.verified_users.mode
);
if let Some(d) = settings.verified_users.verify_update_duration() {
info!("NIP-05 check user verification every: {:?}", d);
}
if let Some(d) = settings.verified_users.verify_expiration_duration() {
info!("NIP-05 user verification expires after: {:?}", d);
}
if let Some(wl) = &settings.verified_users.domain_whitelist {
info!("NIP-05 domain whitelist: {:?}", wl);
}
if let Some(bl) = &settings.verified_users.domain_blacklist {
info!("NIP-05 domain blacklist: {:?}", bl);
}
}
let addr = env::args()
.nth(1)
.unwrap_or_else(|| "0.0.0.0:8888".to_string());
// configure tokio runtime
let rt = Builder::new_multi_thread()
.enable_all()
.worker_threads(2)
.enable_io()
.thread_name("tokio-ws")
.on_thread_stop(|| {
info!("thread stopping");
})
.on_thread_start(|| {
info!("thread starting");
})
.build()
.unwrap();
// start tokio
rt.block_on(async {
let settings = config::SETTINGS.read().unwrap();
info!("listening on: {}", socket_addr);
// all client-submitted valid events are broadcast to every
// other client on this channel. This should be large enough
// to accomodate slower readers (messages are dropped if
// clients can not keep up).
let (bcast_tx, _) = broadcast::channel::<Event>(settings.limits.broadcast_buffer);
// validated events that need to be persisted are sent to the
// database on via this channel.
let (event_tx, event_rx) =
mpsc::channel::<SubmittedEvent>(settings.limits.event_persist_buffer);
// establish a channel for letting all threads now about a
// requested server shutdown.
let (invoke_shutdown, shutdown_listen) = broadcast::channel::<()>(1);
// create a channel for sending any new metadata event. These
// will get processed relatively slowly (a potentially
// multi-second blocking HTTP call) on a single thread, so we
// buffer requests on the channel. No harm in dropping events
// here, since we are protecting against DoS. This can make
// it difficult to setup initial metadata in bulk, since
// overwhelming this will drop events and won't register
// metadata events.
let (metadata_tx, metadata_rx) = broadcast::channel::<Event>(4096);
// start the database writer thread. Give it a channel for
// writing events, and for publishing events that have been
// written (to all connected clients).
db::db_writer(
event_rx,
bcast_tx.clone(),
metadata_tx.clone(),
shutdown_listen,
)
.await;
info!("db writer created");
// create a nip-05 verifier thread
let verifier_opt = nip05::Verifier::new(metadata_rx, bcast_tx.clone());
if let Ok(mut v) = verifier_opt {
if settings.verified_users.is_active() {
tokio::task::spawn(async move {
info!("starting up NIP-05 verifier...");
v.run().await;
});
// Create the event loop and TCP listener we'll accept connections on.
let listener = TcpListener::bind(&addr).await.expect("Failed to bind");
info!("Listening on: {}", addr);
while let Ok((stream, _)) = listener.accept().await {
tokio::spawn(nostr_server(stream));
}
}
// // listen for ctrl-c interruupts
let ctrl_c_shutdown = invoke_shutdown.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.unwrap();
info!("shutting down due to SIGINT");
ctrl_c_shutdown.send(()).ok();
});
// build a connection pool for sqlite connections
let pool = db::build_pool(
"client query",
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY
| rusqlite::OpenFlags::SQLITE_OPEN_SHARED_CACHE,
settings.database.min_conn,
settings.database.max_conn,
true,
);
// A `Service` is needed for every connection, so this
// creates one from our `handle_request` function.
let make_svc = make_service_fn(|conn: &AddrStream| {
let svc_pool = pool.clone();
let remote_addr = conn.remote_addr();
let bcast = bcast_tx.clone();
let event = event_tx.clone();
let stop = invoke_shutdown.clone();
async move {
// service_fn converts our function into a `Service`
Ok::<_, Infallible>(service_fn(move |request: Request<Body>| {
handle_web_request(
request,
svc_pool.clone(),
remote_addr,
bcast.clone(),
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(())
}
/// Nostr protocol messages from a client
#[derive(Deserialize, Serialize, Clone, PartialEq, Debug)]
#[serde(untagged)]
pub enum NostrMessage {
/// An `EVENT` message
EventMsg(EventCmd),
/// A `REQ` message
SubMsg(Subscription),
/// A `CLOSE` message
CloseMsg(CloseCmd),
}
// Todo: Implement sending messages to all other clients; example:
// https://github.com/snapview/tokio-tungstenite/blob/master/examples/server.rs
/// Convert Message to NostrMessage
fn convert_to_msg(msg: String) -> Result<NostrMessage> {
let config = config::SETTINGS.read().unwrap();
let parsed_res: Result<NostrMessage> = serde_json::from_str(&msg).map_err(|e| e.into());
match parsed_res {
Ok(m) => {
if let NostrMessage::EventMsg(_) = m {
if let Some(max_size) = config.limits.max_event_bytes {
// check length, ensure that some max size is set.
if msg.len() > max_size && max_size > 0 {
return Err(Error::EventMaxLengthError(msg.len()));
// Wrap/Upgrade TCP connections in WebSocket Streams, and hand off to Nostr protocol handler.
async fn nostr_server(stream: TcpStream) {
let addr = stream
.peer_addr()
.expect("connected streams should have a peer address");
info!("Peer address: {}", addr);
let config = WebSocketConfig {
max_send_queue: None,
max_message_size: Some(2 << 16), // 64K
max_frame_size: Some(2 << 16), // 64k
accept_unmasked_frames: false, // follow the spec
};
let conn = tokio_tungstenite::accept_async_with_config(stream, Some(config)).await;
match conn {
Ok(ws_stream) => {
info!("New WebSocket connection: {}", addr);
// create a nostr protocol handler, and give it the stream
let mut proto = Proto::new(ws_stream);
proto.process_client().await
}
}
}
Ok(m)
}
Err(e) => {
debug!("proto parse error: {:?}", e);
debug!("parse error on message: {}", msg.trim());
Err(Error::ProtoParseError)
}
}
}
/// Turn a string into a NOTICE message ready to send over a WebSocket
fn make_notice_message(msg: &str) -> Message {
Message::text(json!(["NOTICE", msg]).to_string())
}
/// Handle new client connections. This runs through an event loop
/// for all client communication.
async fn nostr_server(
pool: db::SqlitePool,
mut ws_stream: WebSocketStream<Upgraded>,
broadcast: Sender<Event>,
event_tx: mpsc::Sender<SubmittedEvent>,
mut shutdown: Receiver<()>,
) {
// get a broadcast channel for clients to communicate on
let mut bcast_rx = broadcast.subscribe();
// Track internal client state
let mut conn = conn::ClientConn::new();
let cid = conn.get_client_prefix();
// Create a channel for receiving query results from the database.
// we will send out the tx handle to any query we generate.
let (query_tx, mut query_rx) = mpsc::channel::<db::QueryResult>(256);
// Create channel for receiving NOTICEs
let (notice_tx, mut notice_rx) = mpsc::channel::<String>(32);
// last time this client sent data (message, ping, etc.)
let mut last_message_time = Instant::now();
// ping interval (every 5 minutes)
let default_ping_dur = Duration::from_secs(300);
// disconnect after 20 minutes without a ping response or event.
let max_quiet_time = Duration::from_secs(60 * 20);
let start = tokio::time::Instant::now() + default_ping_dur;
let mut ping_interval = tokio::time::interval_at(start, default_ping_dur);
// maintain a hashmap of a oneshot channel for active subscriptions.
// when these subscriptions are cancelled, make a message
// available to the executing query so it knows to stop.
let mut running_queries: HashMap<String, oneshot::Sender<()>> = HashMap::new();
// for stats, keep track of how many events the client published,
// and how many it received from queries.
let mut client_published_event_count: usize = 0;
let mut client_received_event_count: usize = 0;
info!("new connection for client: {:?}", cid);
loop {
tokio::select! {
_ = shutdown.recv() => {
// server shutting down, exit loop
break;
},
_ = ping_interval.tick() => {
// check how long since we talked to client
// if it has been too long, disconnect
if last_message_time.elapsed() > max_quiet_time {
debug!("ending connection due to lack of client ping response");
break;
}
// Send a ping
ws_stream.send(Message::Ping(Vec::new())).await.ok();
},
Some(notice_msg) = notice_rx.recv() => {
ws_stream.send(make_notice_message(&notice_msg)).await.ok();
},
Some(query_result) = query_rx.recv() => {
// database informed us of a query result we asked for
client_received_event_count += 1;
// send a result
let subesc = query_result.sub_id.replace("\"", "");
let send_str = format!("[\"EVENT\",\"{}\",{}]", subesc, &query_result.event);
ws_stream.send(Message::Text(send_str)).await.ok();
},
// TODO: consider logging the LaggedRecv error
Ok(global_event) = bcast_rx.recv() => {
// an event has been broadcast to all clients
// first check if there is a subscription for this event.
let matching_subs = conn.get_matching_subscriptions(&global_event);
for s in matching_subs {
// TODO: serialize at broadcast time, instead of
// once for each consumer.
if let Ok(event_str) = serde_json::to_string(&global_event) {
debug!("sub match: client: {:?}, sub: {:?}, event: {:?}",
cid, s,
global_event.get_event_id_prefix());
// create an event response and send it
let subesc = s.replace("\"", "");
ws_stream.send(Message::Text(format!("[\"EVENT\",\"{}\",{}]", subesc, event_str))).await.ok();
//nostr_stream.send(res).await.ok();
} else {
warn!("could not serialize event {:?}", global_event.get_event_id_prefix());
}
}
},
ws_next = ws_stream.next() => {
// update most recent message time for client
last_message_time = Instant::now();
// Consume text messages from the client, parse into Nostr messages.
let nostr_msg = match ws_next {
Some(Ok(Message::Text(m))) => {
convert_to_msg(m)
},
Some(Ok(Message::Binary(_))) => {
ws_stream.send(make_notice_message("binary messages are not accepted")).await.ok();
continue;
},
Some(Ok(Message::Ping(_))) | Some(Ok(Message::Pong(_))) => {
// get a ping/pong, ignore. tungstenite will
// send responses automatically.
continue;
},
None |
Some(Ok(Message::Close(_))) |
Some(Err(WsError::AlreadyClosed)) |
Some(Err(WsError::ConnectionClosed)) |
Some(Err(WsError::Protocol(tungstenite::error::ProtocolError::ResetWithoutClosingHandshake)))
=> {
debug!("websocket close from client: {:?}",cid);
break;
},
Some(Err(WsError::Io(e))) => {
// IO errors are considered fatal
warn!("IO error (client: {:?}): {:?}", cid, e);
break;
}
x => {
// default condition on error is to close the client connection
info!("unknown error (client: {:?}): {:?} (closing conn)", cid, x);
break;
Err(_) => {
println!("Error");
info!("Error during websocket handshake");
}
};
// convert ws_next into proto_next
match nostr_msg {
Ok(NostrMessage::EventMsg(ec)) => {
// An EventCmd needs to be validated to be converted into an Event
// handle each type of message
let parsed : Result<Event> = Result::<Event>::from(ec);
match parsed {
Ok(e) => {
let id_prefix:String = e.id.chars().take(8).collect();
debug!("successfully parsed/validated event: {:?} from client: {:?}", id_prefix, cid);
// Write this to the database.
let submit_event = SubmittedEvent { event: e.clone(), notice_tx: notice_tx.clone() };
event_tx.send(submit_event).await.ok();
client_published_event_count += 1;
},
Err(_) => {
info!("client {:?} sent an invalid event", cid);
ws_stream.send(make_notice_message("event was invalid")).await.ok();
}
}
},
Ok(NostrMessage::SubMsg(s)) => {
debug!("client {} requesting a subscription", cid);
// subscription handling consists of:
// * registering the subscription so future events can be matched
// * making a channel to cancel to request later
// * sending a request for a SQL query
let (abandon_query_tx, abandon_query_rx) = oneshot::channel::<()>();
match conn.subscribe(s.clone()) {
Ok(()) => {
// when we insert, if there was a previous query running with the same name, cancel it.
if let Some(previous_query) = running_queries.insert(s.id.to_owned(), abandon_query_tx) {
previous_query.send(()).ok();
}
// start a database query
db::db_query(s, pool.clone(), query_tx.clone(), abandon_query_rx).await;
},
Err(e) => {
info!("Subscription error: {}", e);
ws_stream.send(make_notice_message(&e.to_string())).await.ok();
}
}
},
Ok(NostrMessage::CloseMsg(cc)) => {
// closing a request simply removes the subscription.
let parsed : Result<Close> = Result::<Close>::from(cc);
match parsed {
Ok(c) => {
// check if a query is currently
// running, and remove it if so.
let stop_tx = running_queries.remove(&c.id);
if let Some(tx) = stop_tx {
tx.send(()).ok();
}
// stop checking new events against
// the subscription
conn.unsubscribe(c);
},
Err(_) => {
info!("invalid command ignored");
ws_stream.send(make_notice_message("could not parse command")).await.ok();
}
}
},
Err(Error::ConnError) => {
debug!("got connection close/error, disconnecting client: {:?}",cid);
break;
}
Err(Error::EventMaxLengthError(s)) => {
info!("client {:?} sent event larger ({} bytes) than max size", cid, s);
ws_stream.send(make_notice_message("event exceeded max size")).await.ok();
},
Err(Error::ProtoParseError) => {
info!("client {:?} sent event that could not be parsed", cid);
ws_stream.send(make_notice_message("could not parse command")).await.ok();
},
Err(e) => {
info!("got non-fatal error from client: {:?}, error: {:?}", cid, e);
},
}
},
}
}
// connection cleanup - ensure any still running queries are terminated.
for (_, stop_tx) in running_queries.into_iter() {
stop_tx.send(()).ok();
}
info!(
"stopping connection for client: {:?} (client sent {} event(s), received {})",
cid, client_published_event_count, client_received_event_count
);
}
-818
View File
@@ -1,818 +0,0 @@
//! User verification using NIP-05 names
//!
//! NIP-05 defines a mechanism for authors to associate an internet
//! address with their public key, in metadata events. This module
//! consumes a stream of metadata events, and keeps a database table
//! updated with the current NIP-05 verification status.
use crate::config::SETTINGS;
use crate::db;
use crate::error::{Error, Result};
use crate::event::Event;
use crate::utils::unix_time;
use hyper::body::HttpBody;
use hyper::client::connect::HttpConnector;
use hyper::Client;
use hyper_tls::HttpsConnector;
use log::*;
use rand::Rng;
use rusqlite::params;
use std::time::Duration;
use std::time::Instant;
use std::time::SystemTime;
use tokio::time::Interval;
/// NIP-05 verifier state
pub struct Verifier {
/// Metadata events for us to inspect
metadata_rx: tokio::sync::broadcast::Receiver<Event>,
/// Newly validated events get written and then broadcast on this channel to subscribers
event_tx: tokio::sync::broadcast::Sender<Event>,
/// SQLite read query pool
read_pool: db::SqlitePool,
/// SQLite write query pool
write_pool: db::SqlitePool,
/// HTTP client
client: hyper::Client<HttpsConnector<HttpConnector>, hyper::Body>,
/// After all accounts are updated, wait this long before checking again.
wait_after_finish: Duration,
/// Minimum amount of time between HTTP queries
http_wait_duration: Duration,
/// Interval for updating verification records
reverify_interval: Interval,
}
/// A NIP-05 identifier is a local part and domain.
#[derive(PartialEq, Debug, Clone)]
pub struct Nip05Name {
local: String,
domain: String,
}
impl Nip05Name {
/// Does this name represent the entire domain?
pub fn is_domain_only(&self) -> bool {
self.local == "_"
}
/// Determine the URL to query for verification
fn to_url(&self) -> Option<http::Uri> {
format!(
"https://{}/.well-known/nostr.json?name={}",
self.domain, self.local
)
.parse::<http::Uri>()
.ok()
}
}
// Parsing Nip05Names from strings
impl std::convert::TryFrom<&str> for Nip05Name {
type Error = Error;
fn try_from(inet: &str) -> Result<Self, Self::Error> {
// break full name at the @ boundary.
let components: Vec<&str> = inet.split('@').collect();
if components.len() != 2 {
Err(Error::CustomError("too many/few components".to_owned()))
} else {
// check if local name is valid
let local = components[0];
let domain = components[1];
if local
.chars()
.all(|x| x.is_alphanumeric() || x == '_' || x == '-' || x == '.')
{
if domain
.chars()
.all(|x| x.is_alphanumeric() || x == '-' || x == '.')
{
Ok(Nip05Name {
local: local.to_owned(),
domain: domain.to_owned(),
})
} else {
Err(Error::CustomError(
"invalid character in domain part".to_owned(),
))
}
} else {
Err(Error::CustomError(
"invalid character in local part".to_owned(),
))
}
}
}
}
impl std::fmt::Display for Nip05Name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}@{}", self.local, self.domain)
}
}
// Current time, with a slight foward jitter in seconds
fn now_jitter(sec: u64) -> u64 {
// random time between now, and 10min in future.
let mut rng = rand::thread_rng();
let jitter_amount = rng.gen_range(0..sec);
let now = unix_time();
now.saturating_add(jitter_amount)
}
/// Check if the specified username and address are present and match in this response body
fn body_contains_user(username: &str, address: &str, bytes: hyper::body::Bytes) -> Result<bool> {
// convert the body into json
let body: serde_json::Value = serde_json::from_slice(&bytes)?;
// ensure we have a names object.
let names_map = body
.as_object()
.and_then(|x| x.get("names"))
.and_then(|x| x.as_object())
.ok_or_else(|| Error::CustomError("not a map".to_owned()))?;
// get the pubkey for the requested user
let check_name = names_map.get(username).and_then(|x| x.as_str());
// ensure the address is a match
Ok(check_name.map(|x| x == address).unwrap_or(false))
}
impl Verifier {
pub fn new(
metadata_rx: tokio::sync::broadcast::Receiver<Event>,
event_tx: tokio::sync::broadcast::Sender<Event>,
) -> Result<Self> {
info!("creating NIP-05 verifier");
// build a database connection for reading and writing.
let write_pool = db::build_pool(
"nip05 writer",
rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE,
1, // min conns
4, // max conns
true, // wait for DB
);
let read_pool = db::build_pool(
"nip05 reader",
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
1, // min conns
8, // max conns
true, // wait for DB
);
// setup hyper client
let https = HttpsConnector::new();
let client = Client::builder().build::<_, hyper::Body>(https);
// After all accounts have been re-verified, don't check again
// for this long.
let wait_after_finish = Duration::from_secs(60 * 10);
// when we have an active queue of accounts to validate, we
// will wait this duration between HTTP requests.
let http_wait_duration = Duration::from_secs(1);
// setup initial interval for re-verification. If we find
// there is no work to be done, it will be reset to a longer
// duration.
let reverify_interval = tokio::time::interval(http_wait_duration);
Ok(Verifier {
metadata_rx,
event_tx,
read_pool,
write_pool,
client,
wait_after_finish,
http_wait_duration,
reverify_interval,
})
}
/// Perform web verification against a NIP-05 name and address.
pub async fn get_web_verification(
&mut self,
nip: &Nip05Name,
pubkey: &str,
) -> UserWebVerificationStatus {
self.get_web_verification_res(nip, pubkey)
.await
.unwrap_or(UserWebVerificationStatus::Unknown)
}
/// Perform web verification against an `Event` (must be metadata).
pub async fn get_web_verification_from_event(
&mut self,
e: &Event,
) -> UserWebVerificationStatus {
let nip_parse = e.get_nip05_addr();
if let Some(nip) = nip_parse {
self.get_web_verification_res(&nip, &e.pubkey)
.await
.unwrap_or(UserWebVerificationStatus::Unknown)
} else {
UserWebVerificationStatus::Unknown
}
}
/// Perform web verification, with a `Result` return.
async fn get_web_verification_res(
&mut self,
nip: &Nip05Name,
pubkey: &str,
) -> Result<UserWebVerificationStatus> {
// determine if this domain should be checked
if !is_domain_allowed(&nip.domain) {
return Ok(UserWebVerificationStatus::DomainNotAllowed);
}
let url = nip
.to_url()
.ok_or_else(|| Error::CustomError("invalid NIP-05 URL".to_owned()))?;
let req = hyper::Request::builder()
.method(hyper::Method::GET)
.uri(url)
.header("Accept", "application/json")
.header(
"User-Agent",
format!(
"nostr-rs-relay/{} NIP-05 Verifier",
crate::info::CARGO_PKG_VERSION.unwrap()
),
)
.body(hyper::Body::empty())
.expect("request builder");
let response_fut = self.client.request(req);
// HTTP request with timeout
match tokio::time::timeout(Duration::from_secs(5), response_fut).await {
Ok(response_res) => {
let response = response_res?;
// limit size of verification document to 1MB.
const MAX_ALLOWED_RESPONSE_SIZE: u64 = 1024 * 1024;
// determine content length from response
let response_content_length = match response.body().size_hint().upper() {
Some(v) => v,
None => MAX_ALLOWED_RESPONSE_SIZE + 1, // reject missing content length
};
// TODO: test how hyper handles the client providing an inaccurate content-length.
if response_content_length <= MAX_ALLOWED_RESPONSE_SIZE {
let (parts, body) = response.into_parts();
// TODO: consider redirects
if parts.status == http::StatusCode::OK {
// parse body, determine if the username / key / address is present
let body_bytes = hyper::body::to_bytes(body).await?;
let body_matches = body_contains_user(&nip.local, pubkey, body_bytes)?;
if body_matches {
return Ok(UserWebVerificationStatus::Verified);
} else {
// successful response, parsed as a nip-05
// document, but this name/pubkey was not
// present.
return Ok(UserWebVerificationStatus::Unverified);
}
}
} else {
info!(
"content length missing or exceeded limits for account: {:?}",
nip.to_string()
);
}
}
Err(_) => {
info!("timeout verifying account {:?}", nip);
return Ok(UserWebVerificationStatus::Unknown);
}
}
Ok(UserWebVerificationStatus::Unknown)
}
/// Perform NIP-05 verifier tasks.
pub async fn run(&mut self) {
// use this to schedule periodic re-validation tasks
// run a loop, restarting on failure
loop {
let res = self.run_internal().await;
if let Err(e) = res {
info!("error in verifier: {:?}", e);
}
}
}
/// Internal select loop for performing verification
async fn run_internal(&mut self) -> Result<()> {
tokio::select! {
m = self.metadata_rx.recv() => {
match m {
Ok(e) => {
if let Some(naddr) = e.get_nip05_addr() {
info!("got metadata event for ({:?},{:?})", naddr.to_string() ,e.get_author_prefix());
// Process a new author, checking if they are verified:
let check_verified = get_latest_user_verification(self.read_pool.get().expect("could not get connection"), &e.pubkey).await;
// ensure the event we got is more recent than the one we have, otherwise we can ignore it.
if let Ok(last_check) = check_verified {
if e.created_at <= last_check.event_created {
// this metadata is from the same author as an existing verification.
// it is older than what we have, so we can ignore it.
debug!("received older metadata event for author {:?}", e.get_author_prefix());
return Ok(());
}
}
// old, or no existing record for this user. In either case, we just create a new one.
let start = Instant::now();
let v = self.get_web_verification_from_event(&e).await;
info!(
"checked name {:?}, result: {:?}, in: {:?}",
naddr.to_string(),
v,
start.elapsed()
);
// sleep to limit how frequently we make HTTP requests for new metadata events. This should limit us to 4 req/sec.
tokio::time::sleep(Duration::from_millis(250)).await;
// if this user was verified, we need to write the
// record, persist the event, and broadcast.
if let UserWebVerificationStatus::Verified = v {
self.create_new_verified_user(&naddr.to_string(), &e).await?;
}
}
},
Err(tokio::sync::broadcast::error::RecvError::Lagged(c)) => {
warn!("incoming metadata events overwhelmed buffer, {} events dropped",c);
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
info!("metadata broadcast channel closed");
}
}
},
_ = self.reverify_interval.tick() => {
// check and see if there is an old account that needs
// to be reverified
self.do_reverify().await?;
},
}
Ok(())
}
/// Reverify the oldest user verification record.
async fn do_reverify(&mut self) -> Result<()> {
let reverify_setting;
let max_failures;
{
// this block prevents a read handle to settings being
// captured by the async DB call (guard is not Send)
let settings = SETTINGS.read().unwrap();
reverify_setting = settings.verified_users.verify_update_frequency_duration;
max_failures = settings.verified_users.max_consecutive_failures;
}
// get from settings, but default to 6hrs between re-checking an account
let reverify_dur = reverify_setting.unwrap_or_else(|| Duration::from_secs(60 * 60 * 6));
// find all verification records that have success or failure OLDER than the reverify_dur.
let now = SystemTime::now();
let earliest = now - reverify_dur;
let earliest_epoch = earliest
.duration_since(SystemTime::UNIX_EPOCH)
.map(|x| x.as_secs())
.unwrap_or(0);
let vr = get_oldest_user_verification(self.read_pool.get()?, earliest_epoch).await;
match vr {
Ok(ref v) => {
let new_status = self.get_web_verification(&v.name, &v.address).await;
match new_status {
UserWebVerificationStatus::Verified => {
// freshly verified account, update the
// timestamp.
self.update_verification_record(self.write_pool.get()?, v)
.await?;
}
UserWebVerificationStatus::DomainNotAllowed
| UserWebVerificationStatus::Unknown => {
// server may be offline, or temporarily
// blocked by the config file. Note the
// failure so we can process something
// else.
// have we had enough failures to give up?
if v.failure_count >= max_failures as u64 {
info!(
"giving up on verifying {:?} after {} failures",
v.name, v.failure_count
);
self.delete_verification_record(self.write_pool.get()?, v)
.await?;
} else {
// record normal failure, incrementing failure count
self.fail_verification_record(self.write_pool.get()?, v)
.await?;
}
}
UserWebVerificationStatus::Unverified => {
// domain has removed the verification, drop
// the record on our side.
self.delete_verification_record(self.write_pool.get()?, v)
.await?;
}
}
}
Err(Error::SqlError(rusqlite::Error::QueryReturnedNoRows)) => {
// No users need verification. Reset the interval to
// the next verification attempt.
let start = tokio::time::Instant::now() + self.wait_after_finish;
self.reverify_interval = tokio::time::interval_at(start, self.http_wait_duration);
}
Err(ref e) => {
warn!(
"Error when checking for NIP-05 verification records: {:?}",
e
);
}
}
Ok(())
}
/// Reset the verification timestamp on a VerificationRecord
pub async fn update_verification_record(
&mut self,
mut conn: db::PooledConnection,
vr: &VerificationRecord,
) -> Result<()> {
let vr_id = vr.rowid;
let vr_str = vr.to_string();
tokio::task::spawn_blocking(move || {
// add some jitter to the verification to prevent everything from stacking up together.
let verif_time = now_jitter(600);
let tx = conn.transaction()?;
{
// update verification time and reset any failure count
let query =
"UPDATE user_verification SET verified_at=?, failure_count=0 WHERE id=?";
let mut stmt = tx.prepare(query)?;
stmt.execute(params![verif_time, vr_id])?;
}
tx.commit()?;
info!("verification updated for {}", vr_str);
let ok: Result<()> = Ok(());
ok
})
.await?
}
/// Reset the failure timestamp on a VerificationRecord
pub async fn fail_verification_record(
&mut self,
mut conn: db::PooledConnection,
vr: &VerificationRecord,
) -> Result<()> {
let vr_id = vr.rowid;
let vr_str = vr.to_string();
let fail_count = vr.failure_count.saturating_add(1);
tokio::task::spawn_blocking(move || {
// add some jitter to the verification to prevent everything from stacking up together.
let fail_time = now_jitter(600);
let tx = conn.transaction()?;
{
let query = "UPDATE user_verification SET failed_at=?, failure_count=? WHERE id=?";
let mut stmt = tx.prepare(query)?;
stmt.execute(params![fail_time, fail_count, vr_id])?;
}
tx.commit()?;
info!("verification failed for {}", vr_str);
let ok: Result<()> = Ok(());
ok
})
.await?
}
/// Delete a VerificationRecord that is no longer valid
pub async fn delete_verification_record(
&mut self,
mut conn: db::PooledConnection,
vr: &VerificationRecord,
) -> Result<()> {
let vr_id = vr.rowid;
let vr_str = vr.to_string();
tokio::task::spawn_blocking(move || {
let tx = conn.transaction()?;
{
let query = "DELETE FROM user_verification WHERE id=?;";
let mut stmt = tx.prepare(query)?;
stmt.execute(params![vr_id])?;
}
tx.commit()?;
info!("verification rescinded for {}", vr_str);
let ok: Result<()> = Ok(());
ok
})
.await?
}
/// Persist an event, create a verification record, and broadcast.
// TODO: have more event-writing logic handled in the db module.
// Right now, these events avoid the rate limit. That is
// acceptable since as soon as the user is registered, this path
// is no longer used.
// TODO: refactor these into spawn_blocking
// calls to get them off the async executors.
async fn create_new_verified_user(&mut self, name: &str, event: &Event) -> Result<()> {
let start = Instant::now();
// we should only do this if we are enabled. if we are
// disabled/passive, the event has already been persisted.
let should_write_event;
{
let settings = SETTINGS.read().unwrap();
should_write_event = settings.verified_users.is_enabled()
}
if should_write_event {
match db::write_event(&mut self.write_pool.get()?, event) {
Ok(updated) => {
if updated != 0 {
info!(
"persisted event: {:?} in {:?}",
event.get_event_id_prefix(),
start.elapsed()
);
self.event_tx.send(event.clone()).ok();
}
}
Err(err) => {
warn!("event insert failed: {:?}", err);
if let Error::SqlError(r) = err {
warn!("because: : {:?}", r);
}
}
}
}
// write the verification record
save_verification_record(self.write_pool.get()?, event, name).await?;
Ok(())
}
}
/// Result of checking user's verification status against DNS/HTTP.
#[derive(PartialEq, Debug, Clone)]
pub enum UserWebVerificationStatus {
Verified, // user is verified, as of now.
DomainNotAllowed, // domain blacklist or whitelist denied us from attempting a verification
Unknown, // user's status could not be determined (timeout, server error)
Unverified, // user's status is not verified (successful check, name / addr do not match)
}
/// A NIP-05 verification record.
#[derive(PartialEq, Debug, Clone)]
// Basic information for a verification event. Gives us all we need to assert a NIP-05 address is good.
pub struct VerificationRecord {
pub rowid: u64, // database row for this verification event
pub name: Nip05Name, // address being verified
pub address: String, // pubkey
pub event: String, // event ID hash providing the verification
pub event_created: u64, // when the metadata event was published
pub last_success: Option<u64>, // the most recent time a verification was provided. None if verification under this name has never succeeded.
pub last_failure: Option<u64>, // the most recent time verification was attempted, but could not be completed.
pub failure_count: u64, // how many consecutive failures have been observed.
}
/// Check with settings to determine if a given domain is allowed to
/// publish.
pub fn is_domain_allowed(domain: &str) -> bool {
let settings = SETTINGS.read().unwrap();
// if there is a whitelist, domain must be present in it.
if let Some(wl) = &settings.verified_users.domain_whitelist {
// workaround for Vec contains not accepting &str
return wl.iter().any(|x| x == domain);
}
// otherwise, check that user is not in the blacklist
if let Some(bl) = &settings.verified_users.domain_blacklist {
return !bl.iter().any(|x| x == domain);
}
true
}
impl VerificationRecord {
/// Check if the record is recent enough to be considered valid,
/// and the domain is allowed.
pub fn is_valid(&self) -> bool {
let settings = SETTINGS.read().unwrap();
// how long a verification record is good for
let nip05_expiration = &settings.verified_users.verify_expiration_duration;
if let Some(e) = nip05_expiration {
if !self.is_current(e) {
return false;
}
}
// check domains
is_domain_allowed(&self.name.domain)
}
/// Check if this record has been validated since the given
/// duration.
fn is_current(&self, d: &Duration) -> bool {
match self.last_success {
Some(s) => {
// current time - duration
let now = SystemTime::now();
let cutoff = now - *d;
let cutoff_epoch = cutoff
.duration_since(SystemTime::UNIX_EPOCH)
.map(|x| x.as_secs())
.unwrap_or(0);
s > cutoff_epoch
}
None => false,
}
}
}
impl std::fmt::Display for VerificationRecord {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"({:?},{:?})",
self.name.to_string(),
self.address.chars().take(8).collect::<String>()
)
}
}
/// Create a new verification record based on an event
pub async fn save_verification_record(
mut conn: db::PooledConnection,
event: &Event,
name: &str,
) -> Result<()> {
let e = hex::decode(&event.id).ok();
let n = name.to_owned();
let a_prefix = event.get_author_prefix();
tokio::task::spawn_blocking(move || {
let tx = conn.transaction()?;
{
// if we create a /new/ one, we should get rid of any old ones. or group the new ones by name and only consider the latest.
let query = "INSERT INTO user_verification (metadata_event, name, verified_at) VALUES ((SELECT id from event WHERE event_hash=?), ?, strftime('%s','now'));";
let mut stmt = tx.prepare(query)?;
stmt.execute(params![e, n])?;
// get the row ID
let v_id = tx.last_insert_rowid();
// delete everything else by this name
let del_query = "DELETE FROM user_verification WHERE name = ? AND id != ?;";
let mut del_stmt = tx.prepare(del_query)?;
let count = del_stmt.execute(params![n,v_id])?;
if count > 0 {
info!("removed {} old verification records for ({:?},{:?})", count, n, a_prefix);
}
}
tx.commit()?;
info!("saved new verification record for ({:?},{:?})", n, a_prefix);
let ok: Result<()> = Ok(());
ok
}).await?
}
/// Retrieve the most recent verification record for a given pubkey (async).
pub async fn get_latest_user_verification(
conn: db::PooledConnection,
pubkey: &str,
) -> Result<VerificationRecord> {
let p = pubkey.to_owned();
tokio::task::spawn_blocking(move || query_latest_user_verification(conn, p)).await?
}
/// Query database for the latest verification record for a given pubkey.
pub fn query_latest_user_verification(
mut conn: db::PooledConnection,
pubkey: String,
) -> Result<VerificationRecord> {
let tx = conn.transaction()?;
let query = "SELECT v.id, v.name, e.event_hash, e.created_at, v.verified_at, v.failed_at, v.failure_count FROM user_verification v LEFT JOIN event e ON e.id=v.metadata_event WHERE e.author=? ORDER BY e.created_at DESC, v.verified_at DESC, v.failed_at DESC LIMIT 1;";
let mut stmt = tx.prepare_cached(query)?;
let fields = stmt.query_row(params![hex::decode(&pubkey).ok()], |r| {
let rowid: u64 = r.get(0)?;
let rowname: String = r.get(1)?;
let eventid: Vec<u8> = r.get(2)?;
let created_at: u64 = r.get(3)?;
// create a tuple since we can't throw non-rusqlite errors in this closure
Ok((
rowid,
rowname,
eventid,
created_at,
r.get(4).ok(),
r.get(5).ok(),
r.get(6)?,
))
})?;
Ok(VerificationRecord {
rowid: fields.0,
name: Nip05Name::try_from(&fields.1[..])?,
address: pubkey,
event: hex::encode(fields.2),
event_created: fields.3,
last_success: fields.4,
last_failure: fields.5,
failure_count: fields.6,
})
}
/// Retrieve the oldest user verification (async)
pub async fn get_oldest_user_verification(
conn: db::PooledConnection,
earliest: u64,
) -> Result<VerificationRecord> {
let res =
tokio::task::spawn_blocking(move || query_oldest_user_verification(conn, earliest)).await?;
res
}
pub fn query_oldest_user_verification(
mut conn: db::PooledConnection,
earliest: u64,
) -> Result<VerificationRecord> {
let tx = conn.transaction()?;
let query = "SELECT v.id, v.name, e.event_hash, e.author, e.created_at, v.verified_at, v.failed_at, v.failure_count FROM user_verification v LEFT JOIN event e ON e.id=v.metadata_event WHERE (v.verified_at < ? OR v.verified_at IS NULL) AND (v.failed_at < ? OR v.failed_at IS NULL) ORDER BY v.verified_at ASC, v.failed_at ASC LIMIT 1;";
let mut stmt = tx.prepare_cached(query)?;
let fields = stmt.query_row(params![earliest, earliest], |r| {
let rowid: u64 = r.get(0)?;
let rowname: String = r.get(1)?;
let eventid: Vec<u8> = r.get(2)?;
let pubkey: Vec<u8> = r.get(3)?;
let created_at: u64 = r.get(4)?;
// create a tuple since we can't throw non-rusqlite errors in this closure
Ok((
rowid,
rowname,
eventid,
pubkey,
created_at,
r.get(5).ok(),
r.get(6).ok(),
r.get(7)?,
))
})?;
let vr = VerificationRecord {
rowid: fields.0,
name: Nip05Name::try_from(&fields.1[..])?,
address: hex::encode(fields.3),
event: hex::encode(fields.2),
event_created: fields.4,
last_success: fields.5,
last_failure: fields.6,
failure_count: fields.7,
};
Ok(vr)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_from_inet() {
let addr = "bob@example.com";
let parsed = Nip05Name::try_from(addr);
assert!(!parsed.is_err());
let v = parsed.unwrap();
assert_eq!(v.local, "bob");
assert_eq!(v.domain, "example.com");
}
#[test]
fn not_enough_sep() {
let addr = "bob_example.com";
let parsed = Nip05Name::try_from(addr);
assert!(parsed.is_err());
}
#[test]
fn too_many_sep() {
let addr = "foo@bob@example.com";
let parsed = Nip05Name::try_from(addr);
assert!(parsed.is_err());
}
#[test]
fn invalid_local_name() {
// non-permitted ascii chars
assert!(Nip05Name::try_from("foo!@example.com").is_err());
assert!(Nip05Name::try_from("foo @example.com").is_err());
assert!(Nip05Name::try_from(" foo@example.com").is_err());
assert!(Nip05Name::try_from("f oo@example.com").is_err());
assert!(Nip05Name::try_from("foo<@example.com").is_err());
// unicode dash
assert!(Nip05Name::try_from("foobar@example.com").is_err());
// emoji
assert!(Nip05Name::try_from("foo😭bar@example.com").is_err());
}
#[test]
fn invalid_domain_name() {
// non-permitted ascii chars
assert!(Nip05Name::try_from("foo@examp!e.com").is_err());
assert!(Nip05Name::try_from("foo@ example.com").is_err());
assert!(Nip05Name::try_from("foo@exa mple.com").is_err());
assert!(Nip05Name::try_from("foo@example .com").is_err());
assert!(Nip05Name::try_from("foo@exa<mple.com").is_err());
// unicode dash
assert!(Nip05Name::try_from("foobar@example.com").is_err());
// emoji
assert!(Nip05Name::try_from("foobar@ex😭ample.com").is_err());
}
#[test]
fn to_url() {
let nip = Nip05Name::try_from("foobar@example.com").unwrap();
assert_eq!(
nip.to_url(),
Some(
"https://example.com/.well-known/nostr.json?name=foobar"
.parse()
.unwrap()
)
);
}
}
+234
View File
@@ -0,0 +1,234 @@
use crate::close::Close;
use crate::error::{Error, Result};
use crate::event::Event;
use crate::subscription::Subscription;
use futures::SinkExt;
use tungstenite::error::Error::*;
use tungstenite::protocol::frame::coding::CloseCode;
use tungstenite::protocol::frame::CloseFrame;
use tungstenite::protocol::Message;
use futures::StreamExt;
use log::{debug, info, warn};
use std::collections::HashMap;
use tokio::net::TcpStream;
use tokio_tungstenite::WebSocketStream;
use uuid::Uuid;
// A protocol handler/helper. Use one per client.
pub struct Proto {
client_id: Uuid,
// current set of subscriptions
subscriptions: HashMap<String, Subscription>,
// websocket
stream: WebSocketStream<TcpStream>,
max_subs: usize,
}
const MAX_SUBSCRIPTION_ID_LEN: usize = 256;
impl Proto {
pub fn new(stream: WebSocketStream<TcpStream>) -> Self {
let p = Proto {
client_id: Uuid::new_v4(),
subscriptions: HashMap::new(),
max_subs: 128,
stream: stream,
};
debug!("New client: {:?}", p.client_id);
p
}
pub async fn process_client(&mut self) {
while let Some(mes_res) = self.stream.next().await {
match mes_res {
Ok(Message::Text(cmd)) => {
info!("Message received");
debug!("Message: {}", cmd);
let proto_error = self.process_message(cmd);
match proto_error {
Err(_) => {
self.send_notice("failed to process message.").await;
}
Ok(_) => {
info!("Message processed successfully");
}
}
}
// Everything else below is basically websocket error handling.
Ok(Message::Binary(_)) => {
info!("Ignoring Binary message");
self.send_notice("binary messages not supported.").await;
}
Ok(Message::Ping(_)) | Ok(Message::Pong(_)) => debug!("Ping/Pong"),
Ok(Message::Close(_)) => {
info!("Got request to close connection");
return;
}
Err(tungstenite::error::Error::Capacity(
tungstenite::error::CapacityError::MessageTooLong { size, max_size },
)) => {
info!(
"Message size too large, disconnecting this client. ({} > {})",
size, max_size
);
self.send_notice("binary messages not supported.").await;
self.stream
.close(Some(CloseFrame {
code: CloseCode::Size,
reason: "Exceeded max message size".into(),
}))
.await
.expect("failed to send close frame");
return;
}
Err(AlreadyClosed) => {
warn!("this connection was already closed, and we tried to operate on it");
return;
}
Err(ConnectionClosed) | Err(Io(_)) => {
debug!("Closing this connection normally");
return;
}
Err(Tls(_)) | Err(Protocol(_)) | Err(Utf8) | Err(Url(_)) | Err(HttpFormat(_))
| Err(Http(_)) => {
info!("websocket/tls/enc protocol error, dropping connection");
return;
}
Err(e) => {
warn!("Some new kind of error, bailing: {:?}", e);
return;
}
}
}
}
// sending notice to client. never fails.
pub async fn send_notice(&mut self, msg: &str) {
// TODO: real escaping
let s = msg.replace("\"", "");
self.stream
.send(Message::Text(format!("[\"NOTICE\",\"{}\"]", s)))
.await
.err();
}
// Error results will be transformed into client NOTICEs
pub fn process_message(&mut self, cmd: String) -> Result<()> {
info!(
"Processing message in proto for client: {:?}",
self.client_id
);
let message = parse_cmd(cmd)?;
info!("Parsed message: {:?}", message);
match message {
NostrRequest::EvReq(_) => {}
NostrRequest::SubReq(sub) => self.subscribe(sub),
NostrRequest::CloseReq(close) => self.unsubscribe(close),
};
Ok(())
}
pub fn subscribe(&mut self, s: Subscription) {
// TODO: add NOTICE responses for error conditions. At the
// moment, we are silently dropping subscription requests that
// aren't perfect.
// check if the subscription key is reasonable.
let k = s.get_id();
let sub_id_len = k.len();
if sub_id_len > MAX_SUBSCRIPTION_ID_LEN {
info!("Dropping subscription with huge ({}) length", sub_id_len);
return;
}
// check if an existing subscription exists, and replace if so
if self.subscriptions.contains_key(&k) {
self.subscriptions.remove(&k);
self.subscriptions.insert(k, s);
info!("Replaced existing subscription");
return;
}
// check if there is room for another subscription.
if self.subscriptions.len() >= self.max_subs {
info!("Client has reached the maximum number of unique subscriptions");
return;
}
// add subscription
self.subscriptions.insert(k, s);
info!(
"Registered new subscription, currently have {} active subs",
self.subscriptions.len()
);
}
pub fn unsubscribe(&mut self, c: Close) {
// TODO: return notice if subscription did not exist.
self.subscriptions.remove(&c.get_id());
info!(
"Removed subscription, currently have {} active subs",
self.subscriptions.len()
);
}
}
// A raw message with the expected type
#[derive(PartialEq, Debug)]
pub enum NostrRawMessage {
EvRaw(String),
SubRaw(String),
CloseRaw(String),
}
// A fully parsed request
#[derive(PartialEq, Debug)]
pub enum NostrRequest {
EvReq(Event),
SubReq(Subscription),
CloseReq(Close),
}
// Wrap the message in the expected request type
fn msg_type_wrapper(msg: String) -> Result<NostrRawMessage> {
// check prefix.
if msg.starts_with(r#"["EVENT","#) {
Ok(NostrRawMessage::EvRaw(msg))
} else if msg.starts_with(r#"["REQ","#) {
Ok(NostrRawMessage::SubRaw(msg))
} else if msg.starts_with(r#"["CLOSE","#) {
Ok(NostrRawMessage::CloseRaw(msg))
} else {
Err(Error::CommandNotFound)
}
}
pub fn parse_cmd(msg: String) -> Result<NostrRequest> {
// turn this raw string into a parsed request
let typ = msg_type_wrapper(msg)?;
match typ {
NostrRawMessage::EvRaw(_) => Err(Error::EventParseFailed),
NostrRawMessage::SubRaw(m) => Ok(NostrRequest::SubReq(Subscription::parse(&m)?)),
NostrRawMessage::CloseRaw(m) => Ok(NostrRequest::CloseReq(Close::parse(&m)?)),
}
}
// Parse the request into a fully deserialized type
// The protocol-handling process looks something like:
// Receive a message (bytes).
// Determine the type. We could do this with an untagged deserialization in serde. Or we can peek at the prefix.
// Wrap the message string in the client request type (either Event, Req, Close)
// For Req/Close, we can fully parse these.
// For Event, we want to be more cautious.
// Before we admit an event, we should reject any duplicates.
// duplicates in the datastore will have already been sent out to interested subscribers.
// No point in verifying an event that we already have.
// Event pipeline looks like:
// Get message. (./)
// Verify it is an event. (./)
// Parse into string / number components from JSON.
// Perform validation, re-serialize (or can we re-use the original?)
// Publish to subscribers.
// Push to datastore.
-237
View File
@@ -1,237 +0,0 @@
//! Database schema and migrations
use crate::db::PooledConnection;
use crate::error::Result;
use crate::utils::is_hex;
use log::*;
use rusqlite::limits::Limit;
use rusqlite::params;
use rusqlite::Connection;
// TODO: drop the pubkey_ref and event_ref tables
/// Startup DB Pragmas
pub const STARTUP_SQL: &str = r##"
PRAGMA main.synchronous=NORMAL;
PRAGMA foreign_keys = ON;
pragma mmap_size = 536870912; -- 512MB of mmap
"##;
/// Schema definition
const INIT_SQL: &str = r##"
-- Database settings
PRAGMA encoding = "UTF-8";
PRAGMA journal_mode=WAL;
PRAGMA main.synchronous=NORMAL;
PRAGMA foreign_keys = ON;
PRAGMA application_id = 1654008667;
PRAGMA user_version = 5;
-- Event Table
CREATE TABLE IF NOT EXISTS event (
id INTEGER PRIMARY KEY,
event_hash BLOB NOT NULL, -- 4-byte hash
first_seen INTEGER NOT NULL, -- when the event was first seen (not authored!) (seconds since 1970)
created_at INTEGER NOT NULL, -- when the event was authored
author BLOB NOT NULL, -- author pubkey
kind INTEGER NOT NULL, -- event kind
hidden INTEGER, -- relevant for queries
content TEXT NOT NULL -- serialized json of event object
);
-- Event Indexes
CREATE UNIQUE INDEX IF NOT EXISTS event_hash_index ON event(event_hash);
CREATE INDEX IF NOT EXISTS created_at_index ON event(created_at);
CREATE INDEX IF NOT EXISTS author_index ON event(author);
CREATE INDEX IF NOT EXISTS kind_index ON event(kind);
-- Tag Table
-- Tag values are stored as either a BLOB (if they come in as a
-- hex-string), or TEXT otherwise.
-- This means that searches need to select the appropriate column.
CREATE TABLE IF NOT EXISTS tag (
id INTEGER PRIMARY KEY,
event_id INTEGER NOT NULL, -- an event ID that contains a tag.
name TEXT, -- the tag name ("p", "e", whatever)
value TEXT, -- the tag value, if not hex.
value_hex BLOB, -- the tag value, if it can be interpreted as a hex string.
FOREIGN KEY(event_id) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS tag_val_index ON tag(value);
CREATE INDEX IF NOT EXISTS tag_val_hex_index ON tag(value_hex);
-- NIP-05 User Validation
CREATE TABLE IF NOT EXISTS user_verification (
id INTEGER PRIMARY KEY,
metadata_event INTEGER NOT NULL, -- the metadata event used for this validation.
name TEXT NOT NULL, -- the nip05 field value (user@domain).
verified_at INTEGER, -- timestamp this author/nip05 was most recently verified.
failed_at INTEGER, -- timestamp a verification attempt failed (host down).
failure_count INTEGER DEFAULT 0, -- number of consecutive failures.
FOREIGN KEY(metadata_event) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS user_verification_name_index ON user_verification(name);
CREATE INDEX IF NOT EXISTS user_verification_event_index ON user_verification(metadata_event);
"##;
/// Determine the current application database schema version.
pub fn db_version(conn: &mut Connection) -> Result<usize> {
let query = "PRAGMA user_version;";
let curr_version = conn.query_row(query, [], |row| row.get(0))?;
Ok(curr_version)
}
/// Upgrade DB to latest version, and execute pragma settings
pub fn upgrade_db(conn: &mut PooledConnection) -> Result<()> {
// check the version.
let mut curr_version = db_version(conn)?;
info!("DB version = {:?}", curr_version);
debug!(
"SQLite max query parameters: {}",
conn.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER)
);
debug!(
"SQLite max table/blob/text length: {} MB",
(conn.limit(Limit::SQLITE_LIMIT_LENGTH) as f64 / (1024 * 1024) as f64).floor()
);
debug!(
"SQLite max SQL length: {} MB",
(conn.limit(Limit::SQLITE_LIMIT_SQL_LENGTH) as f64 / (1024 * 1024) as f64).floor()
);
// initialize from scratch
if curr_version == 0 {
match conn.execute_batch(INIT_SQL) {
Ok(()) => {
info!("database pragma/schema initialized to v4, and ready");
}
Err(err) => {
error!("update failed: {}", err);
panic!("database could not be initialized");
}
}
}
if curr_version == 1 {
// only change is adding a hidden column to events.
let upgrade_sql = r##"
ALTER TABLE event ADD hidden INTEGER;
UPDATE event SET hidden=FALSE;
PRAGMA user_version = 2;
"##;
match conn.execute_batch(upgrade_sql) {
Ok(()) => {
info!("database schema upgraded v1 -> v2");
curr_version = 2;
}
Err(err) => {
error!("update failed: {}", err);
panic!("database could not be upgraded");
}
}
}
if curr_version == 2 {
// this version lacks the tag column
info!("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
);
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");
}
if curr_version == 3 {
info!("database schema needs update from 3->4");
let upgrade_sql = r##"
-- incoming metadata events with nip05
CREATE TABLE IF NOT EXISTS user_verification (
id INTEGER PRIMARY KEY,
metadata_event INTEGER NOT NULL, -- the metadata event used for this validation.
name TEXT NOT NULL, -- the nip05 field value (user@domain).
verified_at INTEGER, -- timestamp this author/nip05 was most recently verified.
failed_at INTEGER, -- timestamp a verification attempt failed (host down).
failure_count INTEGER DEFAULT 0, -- number of consecutive failures.
FOREIGN KEY(metadata_event) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS user_verification_name_index ON user_verification(name);
CREATE INDEX IF NOT EXISTS user_verification_event_index ON user_verification(metadata_event);
PRAGMA user_version = 4;
"##;
match conn.execute_batch(upgrade_sql) {
Ok(()) => {
info!("database schema upgraded v3 -> v4");
curr_version = 4;
}
Err(err) => {
error!("update failed: {}", err);
panic!("database could not be upgraded");
}
}
}
if curr_version == 4 {
info!("database schema needs update from 4->5");
let upgrade_sql = r##"
DROP TABLE IF EXISTS event_ref;
DROP TABLE IF EXISTS pubkey_ref;
PRAGMA user_version=5;
"##;
match conn.execute_batch(upgrade_sql) {
Ok(()) => {
info!("database schema upgraded v4 -> v5");
// uncomment if we have a newer version
//curr_version = 5;
}
Err(err) => {
error!("update failed: {}", err);
panic!("database could not be upgraded");
}
}
} else if curr_version == 5 {
debug!("Database version was already current");
} else if curr_version > 5 {
panic!("Database version is newer than supported by this executable");
}
// Setup PRAGMA
conn.execute_batch(STARTUP_SQL)?;
debug!("SQLite PRAGMA startup completed");
Ok(())
}
+118 -261
View File
@@ -1,109 +1,42 @@
//! Subscription and filter parsing
use crate::error::Result;
use crate::error::{Error, Result};
use crate::event::Event;
use serde::de::Unexpected;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::collections::HashSet;
//use serde_json::json;
//use serde_json::Result;
/// Subscription identifier and set of request filters
#[derive(Serialize, PartialEq, Debug, Clone)]
// Container for a request filter
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(transparent)]
pub struct ReqCmd {
cmds: Vec<String>,
}
#[derive(PartialEq, Debug, Clone)]
pub struct Subscription {
pub id: String,
pub filters: Vec<ReqFilter>,
id: String,
filters: Vec<ReqFilter>,
}
/// Filter for requests
///
/// Corresponds to client-provided subscription request elements. Any
/// element can be present if it should be used in filtering, or
/// absent ([`None`]) if it should be ignored.
#[derive(Serialize, PartialEq, Debug, Clone)]
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub struct ReqFilter {
/// Event hashes
pub ids: Option<Vec<String>>,
/// Event kinds
pub kinds: Option<Vec<u64>>,
/// Events published after this time
pub since: Option<u64>,
/// Events published before this time
pub until: Option<u64>,
/// List of author public keys
pub authors: Option<Vec<String>>,
/// Limit number of results
pub limit: Option<u32>,
/// 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,
limit: 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 == "limit" {
rf.limit = 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)
}
id: Option<String>,
author: Option<String>,
kind: Option<u8>,
#[serde(rename = "e#")]
event: Option<String>,
#[serde(rename = "p#")]
pubkey: Option<String>,
since: Option<u64>,
authors: Option<Vec<String>>,
}
impl<'de> Deserialize<'de> for Subscription {
/// Custom deserializer for subscriptions, which have a more
/// complex structure than the other message types.
fn deserialize<D>(deserializer: D) -> Result<Subscription, D::Error>
where
D: Deserializer<'de>,
{
let mut v: Value = Deserialize::deserialize(deserializer)?;
let mut v: serde_json::Value = Deserialize::deserialize(deserializer)?;
// this shoud be a 3-or-more element array.
// verify the first element is a String, REQ
// get the subscription from the second element.
@@ -112,18 +45,18 @@ impl<'de> Deserialize<'de> for Subscription {
// check for array
let va = v
.as_array_mut()
.ok_or_else(|| serde::de::Error::custom("not array"))?;
.ok_or(serde::de::Error::custom("not array"))?;
// check length
if va.len() < 3 {
return Err(serde::de::Error::custom("not enough fields"));
}
let mut i = va.iter_mut();
let mut i = va.into_iter();
// get command ("REQ") and ensure it is a string
let req_cmd_str: serde_json::Value = i.next().unwrap().take();
let req = req_cmd_str
.as_str()
.ok_or_else(|| serde::de::Error::custom("first element of request was not a string"))?;
let req = req_cmd_str.as_str().ok_or(serde::de::Error::custom(
"first element of request was not a string",
))?;
if req != "REQ" {
return Err(serde::de::Error::custom("missing REQ command"));
}
@@ -132,13 +65,12 @@ impl<'de> Deserialize<'de> for Subscription {
let sub_id_str: serde_json::Value = i.next().unwrap().take();
let sub_id = sub_id_str
.as_str()
.ok_or_else(|| serde::de::Error::custom("missing subscription id"))?;
.ok_or(serde::de::Error::custom("missing subscription id"))?;
let mut filters = vec![];
for fv in i {
let f: ReqFilter = serde_json::from_value(fv.take())
.map_err(|_| serde::de::Error::custom("could not parse filter"))?;
// create indexes
filters.push(f);
}
Ok(Subscription {
@@ -149,80 +81,70 @@ impl<'de> Deserialize<'de> for Subscription {
}
impl Subscription {
/// Get a copy of the subscription identifier.
pub fn parse(json: &str) -> Result<Subscription> {
serde_json::from_str(json).map_err(|e| Error::JsonParseFailed(e))
}
pub fn get_id(&self) -> String {
self.id.clone()
}
/// Determine if this subscription matches a given [`Event`]. Any
/// individual filter match is sufficient.
pub fn get_filter_count(&self) -> usize {
self.filters.len()
}
pub fn interested_in_event(&self, event: &Event) -> bool {
// loop through every filter, and return true if any match this event.
for f in self.filters.iter() {
if f.interested_in_event(event) {
return true;
}
}
false
return false;
}
}
fn prefix_match(prefixes: &[String], target: &str) -> bool {
for prefix in prefixes {
if target.starts_with(prefix) {
return true;
}
}
// none matched
false
}
impl ReqFilter {
fn ids_match(&self, event: &Event) -> bool {
self.ids
fn authors_match(&self, author: &str) -> bool {
if self
.authors
.as_ref()
.map(|vs| prefix_match(vs, &event.id))
.map(|vs| vs.contains(&author.to_owned()))
.unwrap_or(true)
}
fn authors_match(&self, event: &Event) -> bool {
self.authors
.as_ref()
.map(|vs| prefix_match(vs, &event.pubkey))
.unwrap_or(true)
}
fn tag_match(&self, event: &Event) -> bool {
// get the hashset from the filter.
if let Some(map) = &self.tags {
for (key, val) in map.iter() {
let tag_match = event.generic_tag_val_intersect(key, val);
// if there is no match for this tag, the match fails.
if !tag_match {
{
return true;
} else {
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)
pub fn interested_in_event(&self, event: &Event) -> bool {
// determine if all populated fields in this filter match the provided event.
// a filter matches an event if all the populated fields match.
// Iterate through each filter field, return false if the field exists and doesn't match the event.
// order based on cost; id, time, kind, author, event
if !self.id.as_ref().map(|v| v == &event.id).unwrap_or(true) {
false
} else if !self.since.map(|t| event.created_at > t).unwrap_or(true) {
false
} else if !self.kind.map(|v| v == event.kind).unwrap_or(true) {
false
} else if !self
.author
.as_ref()
.map(|v| v == &event.pubkey)
.unwrap_or(true)
{
false
} else if !self.authors_match(&event.pubkey) {
false
} else if !self
.event
.as_ref()
.map(|e| event.event_tag_match(e))
.unwrap_or(true)
{
false
} else {
true
}
/// Check if this filter either matches, or does not care about the kind.
fn kind_match(&self, kind: u64) -> bool {
self.kinds
.as_ref()
.map(|ks| ks.contains(&kind))
.unwrap_or(true)
}
/// Determine if all populated fields in this filter match the provided event.
pub fn interested_in_event(&self, event: &Event) -> bool {
// self.id.as_ref().map(|v| v == &event.id).unwrap_or(true)
self.ids_match(event)
&& self.since.map(|t| event.created_at > t).unwrap_or(true)
&& self.until.map(|t| event.created_at < t).unwrap_or(true)
&& self.kind_match(event.kind)
&& self.authors_match(event)
&& self.tag_match(event)
}
}
@@ -236,7 +158,17 @@ mod tests {
let s: Subscription = serde_json::from_str(raw_json)?;
assert_eq!(s.id, "some-id");
assert_eq!(s.filters.len(), 1);
assert_eq!(s.filters.get(0).unwrap().authors, None);
assert_eq!(s.filters.get(0).unwrap().author, None);
Ok(())
}
#[test]
fn multi_empty_request_parse() -> Result<()> {
let raw_json = r#"["REQ","some-id",{}]"#;
let s: Subscription = serde_json::from_str(raw_json)?;
assert_eq!(s.id, "some-id");
assert_eq!(s.filters.len(), 1);
assert_eq!(s.filters.get(0).unwrap().author, None);
Ok(())
}
@@ -253,66 +185,27 @@ mod tests {
}
#[test]
fn legacy_filter() {
// legacy field in filter
let raw_json = "[\"REQ\",\"some-id\",{\"kind\": 3}]";
assert!(serde_json::from_str::<Subscription>(raw_json).is_ok());
fn invalid_filter() {
// unrecognized field in filter
let raw_json = "[\"REQ\",\"some-id\",{\"foo\": 3}]";
assert!(serde_json::from_str::<Subscription>(raw_json).is_err());
}
#[test]
fn author_filter() -> Result<()> {
let raw_json = r#"["REQ","some-id",{"authors": ["test-author-id"]}]"#;
let raw_json = "[\"REQ\",\"some-id\",{\"author\": \"test-author-id\"}]";
let s: Subscription = serde_json::from_str(raw_json)?;
assert_eq!(s.id, "some-id");
assert_eq!(s.filters.len(), 1);
let first_filter = s.filters.get(0).unwrap();
assert_eq!(
first_filter.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));
assert_eq!(first_filter.author, Some("test-author-id".to_owned()));
Ok(())
}
#[test]
fn interest_id_nomatch() -> Result<()> {
// subscription with a filter for ID
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"ids": ["xyz"]}]"#)?;
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"id":"abc"}]"#)?;
let e = Event {
id: "abcde".to_owned(),
pubkey: "".to_owned(),
@@ -321,61 +214,15 @@ mod tests {
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert!(!s.interested_in_event(&e));
Ok(())
}
#[test]
fn interest_until() -> Result<()> {
// subscription with a filter for ID and time
let s: Subscription =
serde_json::from_str(r#"["REQ","xyz",{"ids": ["abc"], "until": 1000}]"#)?;
let e = Event {
id: "abc".to_owned(),
pubkey: "".to_owned(),
created_at: 50,
kind: 0,
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert!(s.interested_in_event(&e));
Ok(())
}
#[test]
fn interest_range() -> Result<()> {
// subscription with a filter for ID and time
let s_in: Subscription =
serde_json::from_str(r#"["REQ","xyz",{"ids": ["abc"], "since": 100, "until": 200}]"#)?;
let s_before: Subscription =
serde_json::from_str(r#"["REQ","xyz",{"ids": ["abc"], "since": 100, "until": 140}]"#)?;
let s_after: Subscription =
serde_json::from_str(r#"["REQ","xyz",{"ids": ["abc"], "since": 160, "until": 200}]"#)?;
let e = Event {
id: "abc".to_owned(),
pubkey: "".to_owned(),
created_at: 150,
kind: 0,
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert!(s_in.interested_in_event(&e));
assert!(!s_before.interested_in_event(&e));
assert!(!s_after.interested_in_event(&e));
assert_eq!(s.interested_in_event(&e), false);
Ok(())
}
#[test]
fn interest_time_and_id() -> Result<()> {
// subscription with a filter for ID and time
let s: Subscription =
serde_json::from_str(r#"["REQ","xyz",{"ids": ["abc"], "since": 1000}]"#)?;
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"id":"abc", "since": 1000}]"#)?;
let e = Event {
id: "abc".to_owned(),
pubkey: "".to_owned(),
@@ -384,9 +231,8 @@ mod tests {
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert!(!s.interested_in_event(&e));
assert_eq!(s.interested_in_event(&e), false);
Ok(())
}
@@ -402,9 +248,8 @@ mod tests {
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert!(s.interested_in_event(&e));
assert_eq!(s.interested_in_event(&e), true);
Ok(())
}
@@ -420,9 +265,25 @@ mod tests {
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert!(s.interested_in_event(&e));
assert_eq!(s.interested_in_event(&e), true);
Ok(())
}
#[test]
fn author_single() -> Result<()> {
// subscription with a filter for ID
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"author":"abc"}]"#)?;
let e = Event {
id: "123".to_owned(),
pubkey: "abc".to_owned(),
created_at: 0,
kind: 0,
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
};
assert_eq!(s.interested_in_event(&e), true);
Ok(())
}
@@ -438,13 +299,11 @@ mod tests {
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert!(s.interested_in_event(&e));
assert_eq!(s.interested_in_event(&e), true);
Ok(())
}
#[test]
fn authors_multi_pubkey() -> Result<()> {
// check for any of a set of authors, against the pubkey
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"authors":["abc", "bcd"]}]"#)?;
@@ -456,9 +315,8 @@ mod tests {
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert!(s.interested_in_event(&e));
assert_eq!(s.interested_in_event(&e), true);
Ok(())
}
@@ -474,9 +332,8 @@ mod tests {
tags: Vec::new(),
content: "".to_owned(),
sig: "".to_owned(),
tagidx: None,
};
assert!(!s.interested_in_event(&e));
assert_eq!(s.interested_in_event(&e), false);
Ok(())
}
}
-15
View File
@@ -1,15 +0,0 @@
//! Common utility functions
use std::time::SystemTime;
/// Seconds since 1970.
pub fn unix_time() -> u64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|x| x.as_secs())
.unwrap_or(0)
}
/// Check if a string contains only hex characters.
pub fn is_hex(s: &str) -> bool {
s.chars().all(|x| char::is_ascii_hexdigit(&x))
}