mirror of
https://github.com/mikedilger/chorus.git
synced 2026-05-03 06:51:42 +00:00
Compare commits
11 Commits
72c874ff37
...
61fcbcf257
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61fcbcf257 | ||
|
|
4ff23fb2a6 | ||
|
|
1f2c33d532 | ||
|
|
a1e52a2d07 | ||
|
|
b68431bba0 | ||
|
|
ce612a1fc5 | ||
|
|
6f77156684 | ||
|
|
17f9687b4c | ||
|
|
3f15894aaa | ||
|
|
2ca4b17b73 | ||
|
|
e5251e0f57 |
1282
Cargo.lock
generated
1282
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "chorus"
|
name = "chorus"
|
||||||
version = "2.0.0"
|
version = "2.0.1"
|
||||||
description = "A personal relay for nostr"
|
description = "A personal relay for nostr"
|
||||||
authors = ["Mike Dilger <mike@mikedilger.com>"]
|
authors = ["Mike Dilger <mike@mikedilger.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@ -9,7 +9,7 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
bitcoin_hashes = { version = "0.16", features = [ "bitcoin-io" ] }
|
bitcoin_hashes = "0.19"
|
||||||
dashmap = "6"
|
dashmap = "6"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
@ -24,12 +24,12 @@ log = "0.4"
|
|||||||
mime-sniffer = "0.1"
|
mime-sniffer = "0.1"
|
||||||
mime2ext = "0.1"
|
mime2ext = "0.1"
|
||||||
negentropy = "0.5"
|
negentropy = "0.5"
|
||||||
pocket-types = { git = "https://github.com/mikedilger/pocket", branch = "master" }
|
pocket-types = { git = "https://github.com/mikedilger/pocket", ref = "43d35015f7caf1db48eb846a1d6916a5716048da" }
|
||||||
pocket-db = { git = "https://github.com/mikedilger/pocket", branch = "master" }
|
pocket-db = { git = "https://github.com/mikedilger/pocket", ref = "43d35015f7caf1db48eb846a1d6916a5716048da" }
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
rustls-pki-types = "1.11"
|
rustls-pki-types = "1.11"
|
||||||
rustls-pemfile = "2.2"
|
rustls-pemfile = "2.2"
|
||||||
secp256k1 = { version = "0.30", features = [ "hashes", "global-context" ] }
|
secp256k1 = { version = "0.31", features = [ "hashes", "global-context" ] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
speedy = "0.8"
|
speedy = "0.8"
|
||||||
|
|||||||
@ -97,6 +97,14 @@ key_pem_path = "/opt/chorus/etc/tls/privkey.pem"
|
|||||||
# description = "A default config of the Chorus relay"
|
# description = "A default config of the Chorus relay"
|
||||||
|
|
||||||
|
|
||||||
|
# This is a banner URL pointing to an image representing your relay, displayed in the NIP-11
|
||||||
|
# response.
|
||||||
|
#
|
||||||
|
# Default is not set
|
||||||
|
#
|
||||||
|
# banner_url =
|
||||||
|
|
||||||
|
|
||||||
# This is an icon URL pointing to an image representing your relay, displayed in the NIP-11
|
# This is an icon URL pointing to an image representing your relay, displayed in the NIP-11
|
||||||
# response.
|
# response.
|
||||||
#
|
#
|
||||||
@ -105,6 +113,20 @@ key_pem_path = "/opt/chorus/etc/tls/privkey.pem"
|
|||||||
# icon_url =
|
# icon_url =
|
||||||
|
|
||||||
|
|
||||||
|
# This is an optional privacy policy as a blob of text (not a URL, not HTML).
|
||||||
|
#
|
||||||
|
# Default is not set
|
||||||
|
#
|
||||||
|
# privacy_policy = ""
|
||||||
|
|
||||||
|
|
||||||
|
# This is an optional terms of service document as a blob of text (not a URL, not HTML).
|
||||||
|
#
|
||||||
|
# Default is not set
|
||||||
|
#
|
||||||
|
# terms_of_service = ""
|
||||||
|
|
||||||
|
|
||||||
# This is an optional contact for your relay, displayed in the NIP-11 response.
|
# This is an optional contact for your relay, displayed in the NIP-11 response.
|
||||||
#
|
#
|
||||||
# Default is not set
|
# Default is not set
|
||||||
|
|||||||
@ -94,10 +94,22 @@ This is an optional description for your relay, displayed in the NIP-11 response
|
|||||||
|
|
||||||
Default is "A default config of the Chorus relay".
|
Default is "A default config of the Chorus relay".
|
||||||
|
|
||||||
|
### banner_url
|
||||||
|
|
||||||
|
This is an optional URL for an graphical banner representing your relay, displayed in the NIP-11 response.
|
||||||
|
|
||||||
### icon_url
|
### icon_url
|
||||||
|
|
||||||
This is an optional URL for an graphical icon representing your relay, displayed in the NIP-11 response.
|
This is an optional URL for an graphical icon representing your relay, displayed in the NIP-11 response.
|
||||||
|
|
||||||
|
### privacy_policy
|
||||||
|
|
||||||
|
This is an optional privacy policy as a blob of text (not a URL, not HTML).
|
||||||
|
|
||||||
|
### terms_of_service
|
||||||
|
|
||||||
|
This is an optional terms of service document as a blob of text (not a URL, not HTML).
|
||||||
|
|
||||||
### contact
|
### contact
|
||||||
|
|
||||||
This is an optional administrative contact for your relay, displayed in the NIP-11 response.
|
This is an optional administrative contact for your relay, displayed in the NIP-11 response.
|
||||||
|
|||||||
@ -83,7 +83,8 @@ Go ahead and edit that file to your liking. In particular:
|
|||||||
- Change the `ip_address` to your internet-accessible IP address (if you are running directly)
|
- Change the `ip_address` to your internet-accessible IP address (if you are running directly)
|
||||||
or to 127.0.0.1 with a local port like 8080 (if you are proxying behind nginx)
|
or to 127.0.0.1 with a local port like 8080 (if you are proxying behind nginx)
|
||||||
- Change the port if necessary
|
- Change the port if necessary
|
||||||
- Change the name, description, icon_url and contact (e.g. your email address) as desired
|
- Change the name, description, banner_url, icon_url, privacy_policy, terms_of_service and
|
||||||
|
contact (e.g. your email address) as desired
|
||||||
- Set your contact_public_key_hex (it is an option, so use `Some()`)
|
- Set your contact_public_key_hex (it is an option, so use `Some()`)
|
||||||
- Set hex keys of users for which this relay will act as a personal relay
|
- Set hex keys of users for which this relay will act as a personal relay
|
||||||
|
|
||||||
|
|||||||
@ -10,10 +10,10 @@
|
|||||||
2) Users and moderators are now dynamically configured in the database. Use `chorus_cmd` to
|
2) Users and moderators are now dynamically configured in the database. Use `chorus_cmd` to
|
||||||
manage them from the command line:
|
manage them from the command line:
|
||||||
|
|
||||||
* Adding a user: `chorus_cmd add_user <pubkey> 0`
|
* Adding a user: `chorus_cmd <chorus.toml> add_user <pubkey> 0`
|
||||||
* Adding a moderator: `chorus_cmd add_user <pubkey> 1`
|
* Adding a moderator: `chorus_cmd <chorus.toml> add_user <pubkey> 1`
|
||||||
* Removing a user or moderator: `chorus_cmd rm_user <pubkey>`
|
* Removing a user or moderator: `chorus_cmd <chorus.toml> rm_user <pubkey>`
|
||||||
* Listing users and moderators: `chorus_cmd dump_users`
|
* Listing users and moderators: `chorus_cmd <chorus.toml> dump_users`
|
||||||
|
|
||||||
3) Remove the following from your config file as these are no longer used:
|
3) Remove the following from your config file as these are no longer used:
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,8 @@ certchain_pem_path = "tls/fullchain.pem"
|
|||||||
key_pem_path = "tls/privkey.pem"
|
key_pem_path = "tls/privkey.pem"
|
||||||
name = "Chorus Sample"
|
name = "Chorus Sample"
|
||||||
description = "A sample run of the Chorus relay"
|
description = "A sample run of the Chorus relay"
|
||||||
|
privacy_policy = "This relay guarantees nothing. Privacy is your concern, not ours."
|
||||||
|
terms_of_service = "This relay guarantees nothing. Use at your own risk."
|
||||||
# icon_url =
|
# icon_url =
|
||||||
open_relay = false
|
open_relay = false
|
||||||
admin_hex_keys = [
|
admin_hex_keys = [
|
||||||
|
|||||||
@ -10,7 +10,7 @@ fn main() -> Result<(), Error> {
|
|||||||
// Get args (config path)
|
// Get args (config path)
|
||||||
let mut args = env::args();
|
let mut args = env::args();
|
||||||
if args.len() <= 1 {
|
if args.len() <= 1 {
|
||||||
panic!("USAGE: chorus_moderate <config_path>");
|
panic!("USAGE: chorus_cmd <config_path> <command> [args...]");
|
||||||
}
|
}
|
||||||
let _ = args.next(); // ignore program name
|
let _ = args.next(); // ignore program name
|
||||||
|
|
||||||
|
|||||||
27
src/bin/chorus_init.rs
Normal file
27
src/bin/chorus_init.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
use chorus::error::Error;
|
||||||
|
use chorus::globals::GLOBALS;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Error> {
|
||||||
|
// Get args (config path)
|
||||||
|
let mut args = env::args();
|
||||||
|
if args.len() <= 1 {
|
||||||
|
panic!("USAGE: chorus <config_path>");
|
||||||
|
}
|
||||||
|
let _ = args.next(); // ignore program name
|
||||||
|
let config_path = args.next().unwrap();
|
||||||
|
|
||||||
|
let config = chorus::load_config(&config_path)?;
|
||||||
|
|
||||||
|
chorus::setup_logging(&config);
|
||||||
|
|
||||||
|
// Log host name
|
||||||
|
log::info!(target: "Server", "HOSTNAME = {}", config.hostname);
|
||||||
|
|
||||||
|
chorus::setup_store(&config)?;
|
||||||
|
|
||||||
|
let _ = GLOBALS.store.get().unwrap().sync();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -19,7 +19,10 @@ pub struct FriendlyConfig {
|
|||||||
pub key_pem_path: String,
|
pub key_pem_path: String,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
pub banner_url: Option<String>,
|
||||||
pub icon_url: Option<String>,
|
pub icon_url: Option<String>,
|
||||||
|
pub privacy_policy: Option<String>,
|
||||||
|
pub terms_of_service: Option<String>,
|
||||||
pub contact: Option<String>,
|
pub contact: Option<String>,
|
||||||
#[serde(alias = "public_key_hex")]
|
#[serde(alias = "public_key_hex")]
|
||||||
pub contact_public_key_hex: Option<String>,
|
pub contact_public_key_hex: Option<String>,
|
||||||
@ -60,7 +63,10 @@ impl Default for FriendlyConfig {
|
|||||||
key_pem_path: "/opt/chorus/etc/tls/privkey.pem".to_string(),
|
key_pem_path: "/opt/chorus/etc/tls/privkey.pem".to_string(),
|
||||||
name: Some("Chorus Default".to_string()),
|
name: Some("Chorus Default".to_string()),
|
||||||
description: Some("A default config of the Chorus relay".to_string()),
|
description: Some("A default config of the Chorus relay".to_string()),
|
||||||
|
banner_url: None,
|
||||||
icon_url: None,
|
icon_url: None,
|
||||||
|
privacy_policy: None,
|
||||||
|
terms_of_service: None,
|
||||||
contact: None,
|
contact: None,
|
||||||
contact_public_key_hex: None,
|
contact_public_key_hex: None,
|
||||||
open_relay: false,
|
open_relay: false,
|
||||||
@ -102,7 +108,10 @@ impl FriendlyConfig {
|
|||||||
key_pem_path,
|
key_pem_path,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
banner_url,
|
||||||
icon_url,
|
icon_url,
|
||||||
|
privacy_policy,
|
||||||
|
terms_of_service,
|
||||||
contact,
|
contact,
|
||||||
contact_public_key_hex,
|
contact_public_key_hex,
|
||||||
open_relay,
|
open_relay,
|
||||||
@ -159,7 +168,10 @@ impl FriendlyConfig {
|
|||||||
key_pem_path,
|
key_pem_path,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
banner_url,
|
||||||
icon_url,
|
icon_url,
|
||||||
|
privacy_policy,
|
||||||
|
terms_of_service,
|
||||||
contact,
|
contact,
|
||||||
contact_public_key,
|
contact_public_key,
|
||||||
open_relay,
|
open_relay,
|
||||||
@ -201,7 +213,10 @@ pub struct Config {
|
|||||||
pub key_pem_path: String,
|
pub key_pem_path: String,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
pub banner_url: Option<String>,
|
||||||
pub icon_url: Option<String>,
|
pub icon_url: Option<String>,
|
||||||
|
pub privacy_policy: Option<String>,
|
||||||
|
pub terms_of_service: Option<String>,
|
||||||
pub contact: Option<String>,
|
pub contact: Option<String>,
|
||||||
pub contact_public_key: Option<Pubkey>,
|
pub contact_public_key: Option<Pubkey>,
|
||||||
pub open_relay: bool,
|
pub open_relay: bool,
|
||||||
@ -238,6 +253,8 @@ impl Default for Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
/// Get the URI for our server matching the inner Uri, overridden with either
|
||||||
|
/// our base_url parts or our hostname/port.
|
||||||
pub fn uri_parts(&self, inner: Uri, http: bool) -> Result<http::uri::Parts, Error> {
|
pub fn uri_parts(&self, inner: Uri, http: bool) -> Result<http::uri::Parts, Error> {
|
||||||
let mut uri_parts = inner.into_parts();
|
let mut uri_parts = inner.into_parts();
|
||||||
|
|
||||||
|
|||||||
34
src/error.rs
34
src/error.rs
@ -45,7 +45,7 @@ pub enum ChorusError {
|
|||||||
BannedUser,
|
BannedUser,
|
||||||
|
|
||||||
// Base64 Decode Error
|
// Base64 Decode Error
|
||||||
Base64Decode(base64::DecodeError),
|
Base64Decode(base64::DecodeError), // 24b
|
||||||
|
|
||||||
// Blocked IP
|
// Blocked IP
|
||||||
BlockedIp,
|
BlockedIp,
|
||||||
@ -54,16 +54,16 @@ pub enum ChorusError {
|
|||||||
BlossomAuthFailure(String),
|
BlossomAuthFailure(String),
|
||||||
|
|
||||||
// Channel Recv
|
// Channel Recv
|
||||||
ChannelRecv(tokio::sync::broadcast::error::RecvError),
|
ChannelRecv(tokio::sync::broadcast::error::RecvError), // 24b
|
||||||
|
|
||||||
// Channel Send
|
// Channel Send
|
||||||
ChannelSend(tokio::sync::broadcast::error::SendError<u64>),
|
ChannelSend(tokio::sync::broadcast::error::SendError<u64>), // 16b
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
Config(toml::de::Error),
|
Config(Box<toml::de::Error>), // 16b
|
||||||
|
|
||||||
// Crypto
|
// Crypto
|
||||||
Crypto(secp256k1::Error),
|
Crypto(secp256k1::Error), // 16b
|
||||||
|
|
||||||
// Closing on error(s)
|
// Closing on error(s)
|
||||||
ErrorClose,
|
ErrorClose,
|
||||||
@ -72,16 +72,16 @@ pub enum ChorusError {
|
|||||||
EventIsInvalid(String),
|
EventIsInvalid(String),
|
||||||
|
|
||||||
// From hex
|
// From hex
|
||||||
FromHex(hex::FromHexError),
|
FromHex(hex::FromHexError), // 24b
|
||||||
|
|
||||||
// From UTF8
|
// From UTF8
|
||||||
FromUtf8(std::string::FromUtf8Error),
|
FromUtf8(Box<std::string::FromUtf8Error>),
|
||||||
|
|
||||||
// General
|
// General
|
||||||
General(String),
|
General(String),
|
||||||
|
|
||||||
// Http
|
// Http
|
||||||
Http(hyper::http::Error),
|
Http(Box<hyper::http::Error>),
|
||||||
|
|
||||||
// Hyper
|
// Hyper
|
||||||
Hyper(hyper::Error),
|
Hyper(hyper::Error),
|
||||||
@ -126,7 +126,7 @@ pub enum ChorusError {
|
|||||||
PocketDb(pocket_db::Error),
|
PocketDb(pocket_db::Error),
|
||||||
|
|
||||||
// Pocket Db Heed Error
|
// Pocket Db Heed Error
|
||||||
PocketDbHeed(pocket_db::heed::Error),
|
PocketDbHeed(Box<pocket_db::heed::Error>),
|
||||||
|
|
||||||
// Pocket Types Error
|
// Pocket Types Error
|
||||||
PocketType(pocket_types::Error),
|
PocketType(pocket_types::Error),
|
||||||
@ -141,7 +141,7 @@ pub enum ChorusError {
|
|||||||
Restricted,
|
Restricted,
|
||||||
|
|
||||||
// Rustls
|
// Rustls
|
||||||
Rustls(tokio_rustls::rustls::Error),
|
Rustls(Box<tokio_rustls::rustls::Error>),
|
||||||
|
|
||||||
// Filter is underspecified
|
// Filter is underspecified
|
||||||
Scraper,
|
Scraper,
|
||||||
@ -165,7 +165,7 @@ pub enum ChorusError {
|
|||||||
TooManySubscriptions,
|
TooManySubscriptions,
|
||||||
|
|
||||||
// Tungstenite
|
// Tungstenite
|
||||||
Tungstenite(hyper_tungstenite::tungstenite::error::Error),
|
Tungstenite(Box<hyper_tungstenite::tungstenite::error::Error>),
|
||||||
|
|
||||||
// URL Parse
|
// URL Parse
|
||||||
UrlParse(url::ParseError),
|
UrlParse(url::ParseError),
|
||||||
@ -382,7 +382,7 @@ impl From<toml::de::Error> for Error {
|
|||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn from(err: toml::de::Error) -> Self {
|
fn from(err: toml::de::Error) -> Self {
|
||||||
Error {
|
Error {
|
||||||
inner: ChorusError::Config(err),
|
inner: ChorusError::Config(Box::new(err)),
|
||||||
location: std::panic::Location::caller(),
|
location: std::panic::Location::caller(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -402,7 +402,7 @@ impl From<hyper::http::Error> for Error {
|
|||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn from(err: hyper::http::Error) -> Self {
|
fn from(err: hyper::http::Error) -> Self {
|
||||||
Error {
|
Error {
|
||||||
inner: ChorusError::Http(err),
|
inner: ChorusError::Http(Box::new(err)),
|
||||||
location: std::panic::Location::caller(),
|
location: std::panic::Location::caller(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -472,7 +472,7 @@ impl From<pocket_db::heed::Error> for Error {
|
|||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn from(err: pocket_db::heed::Error) -> Self {
|
fn from(err: pocket_db::heed::Error) -> Self {
|
||||||
Error {
|
Error {
|
||||||
inner: ChorusError::PocketDbHeed(err),
|
inner: ChorusError::PocketDbHeed(Box::new(err)),
|
||||||
location: std::panic::Location::caller(),
|
location: std::panic::Location::caller(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -492,7 +492,7 @@ impl From<tokio_rustls::rustls::Error> for Error {
|
|||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn from(err: tokio_rustls::rustls::Error) -> Self {
|
fn from(err: tokio_rustls::rustls::Error) -> Self {
|
||||||
Error {
|
Error {
|
||||||
inner: ChorusError::Rustls(err),
|
inner: ChorusError::Rustls(Box::new(err)),
|
||||||
location: std::panic::Location::caller(),
|
location: std::panic::Location::caller(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -502,7 +502,7 @@ impl From<hyper_tungstenite::tungstenite::error::Error> for Error {
|
|||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn from(err: hyper_tungstenite::tungstenite::error::Error) -> Self {
|
fn from(err: hyper_tungstenite::tungstenite::error::Error) -> Self {
|
||||||
Error {
|
Error {
|
||||||
inner: ChorusError::Tungstenite(err),
|
inner: ChorusError::Tungstenite(Box::new(err)),
|
||||||
location: std::panic::Location::caller(),
|
location: std::panic::Location::caller(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -572,7 +572,7 @@ impl From<std::string::FromUtf8Error> for Error {
|
|||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn from(err: std::string::FromUtf8Error) -> Self {
|
fn from(err: std::string::FromUtf8Error) -> Self {
|
||||||
Error {
|
Error {
|
||||||
inner: ChorusError::FromUtf8(err),
|
inner: ChorusError::FromUtf8(Box::new(err)),
|
||||||
location: std::panic::Location::caller(),
|
location: std::panic::Location::caller(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
src/lib.rs
47
src/lib.rs
@ -180,9 +180,9 @@ async fn handle_http_request(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut web_socket_config = WebSocketConfig::default();
|
let mut web_socket_config = WebSocketConfig::default();
|
||||||
web_socket_config.max_write_buffer_size = 1024 * 1024; // 1 MB
|
web_socket_config.max_write_buffer_size = 1024 * 1024; // 1 MB
|
||||||
web_socket_config.max_message_size = Some(1024 * 1024); // 1 MB
|
web_socket_config.max_message_size = Some(1024 * 1024); // 1 MB
|
||||||
web_socket_config.max_frame_size = Some(1024 * 1024); // 1 MB
|
web_socket_config.max_frame_size = Some(1024 * 1024); // 1 MB
|
||||||
|
|
||||||
let (mut response, websocket) =
|
let (mut response, websocket) =
|
||||||
hyper_tungstenite::upgrade(&mut request, Some(web_socket_config))?;
|
hyper_tungstenite::upgrade(&mut request, Some(web_socket_config))?;
|
||||||
@ -220,7 +220,7 @@ async fn websocket_thread(peer: HashedPeer, websocket: HyperWebsocket, origin: S
|
|||||||
last_message: Instant::now(),
|
last_message: Instant::now(),
|
||||||
burst_tokens: GLOBALS.config.read().throttling_burst,
|
burst_tokens: GLOBALS.config.read().throttling_burst,
|
||||||
challenge: TextNonce::new().into_string(),
|
challenge: TextNonce::new().into_string(),
|
||||||
user: None,
|
authed_as: Vec::new(),
|
||||||
error_punishment: 0.0,
|
error_punishment: 0.0,
|
||||||
replied: false,
|
replied: false,
|
||||||
negentropy_sub: None,
|
negentropy_sub: None,
|
||||||
@ -254,21 +254,30 @@ async fn websocket_thread(peer: HashedPeer, websocket: HyperWebsocket, origin: S
|
|||||||
// Handle the websocket
|
// Handle the websocket
|
||||||
if let Err(e) = ws_service.handle_websocket_stream().await {
|
if let Err(e) = ws_service.handle_websocket_stream().await {
|
||||||
match e.inner {
|
match e.inner {
|
||||||
ChorusError::Tungstenite(tungstenite::error::Error::Protocol(
|
ChorusError::Tungstenite(ref t) => {
|
||||||
tungstenite::error::ProtocolError::ResetWithoutClosingHandshake,
|
match *t.as_ref() {
|
||||||
)) => {
|
tungstenite::error::Error::Protocol(
|
||||||
// So they disconnected ungracefully.
|
tungstenite::error::ProtocolError::ResetWithoutClosingHandshake,
|
||||||
// No big deal, still SessionExit::Ok
|
) => {
|
||||||
msg = "Reset";
|
// So they disconnected ungracefully.
|
||||||
}
|
// No big deal, still SessionExit::Ok
|
||||||
ChorusError::Tungstenite(tungstenite::error::Error::Io(ref ioerror)) => {
|
|
||||||
match ioerror.kind() {
|
|
||||||
std::io::ErrorKind::ConnectionReset
|
|
||||||
| std::io::ErrorKind::ConnectionAborted
|
|
||||||
| std::io::ErrorKind::UnexpectedEof => {
|
|
||||||
// no biggie.
|
|
||||||
msg = "Reset";
|
msg = "Reset";
|
||||||
}
|
}
|
||||||
|
tungstenite::error::Error::Io(ref ioerror) => {
|
||||||
|
match ioerror.kind() {
|
||||||
|
std::io::ErrorKind::ConnectionReset
|
||||||
|
| std::io::ErrorKind::ConnectionAborted
|
||||||
|
| std::io::ErrorKind::UnexpectedEof => {
|
||||||
|
// no biggie.
|
||||||
|
msg = "Reset";
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::error!(target: "Client", "{}: {}", peer, e);
|
||||||
|
session_exit = SessionExit::ErrorExit;
|
||||||
|
msg = "Error Exited";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::error!(target: "Client", "{}: {}", peer, e);
|
log::error!(target: "Client", "{}: {}", peer, e);
|
||||||
session_exit = SessionExit::ErrorExit;
|
session_exit = SessionExit::ErrorExit;
|
||||||
@ -356,7 +365,7 @@ struct WebSocketService {
|
|||||||
pub last_message: Instant,
|
pub last_message: Instant,
|
||||||
pub burst_tokens: usize,
|
pub burst_tokens: usize,
|
||||||
pub challenge: String,
|
pub challenge: String,
|
||||||
pub user: Option<Pubkey>,
|
pub authed_as: Vec<Pubkey>,
|
||||||
pub error_punishment: f32,
|
pub error_punishment: f32,
|
||||||
pub replied: bool,
|
pub replied: bool,
|
||||||
pub negentropy_sub: Option<String>,
|
pub negentropy_sub: Option<String>,
|
||||||
@ -473,8 +482,8 @@ impl WebSocketService {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.get_event_by_offset(new_event_offset)?;
|
.get_event_by_offset(new_event_offset)?;
|
||||||
|
|
||||||
let event_flags = nostr::event_flags(event, &self.user);
|
let event_flags = nostr::event_flags(event, self.authed_as.as_slice());
|
||||||
let authorized_user = self.user.map(is_authorized_user).unwrap_or(false);
|
let authorized_user = self.authed_as.iter().any(|u| is_authorized_user(*u));
|
||||||
|
|
||||||
'subs: for (subid, filters) in self.subscriptions.iter() {
|
'subs: for (subid, filters) in self.subscriptions.iter() {
|
||||||
for filter in filters.iter() {
|
for filter in filters.iter() {
|
||||||
|
|||||||
38
src/nostr.rs
38
src/nostr.rs
@ -124,10 +124,9 @@ impl WebSocketService {
|
|||||||
return Err(ChorusError::TooManySubscriptions.into());
|
return Err(ChorusError::TooManySubscriptions.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = self.user;
|
let authorized_user = self.authed_as.iter().any(|u| crate::is_authorized_user(*u));
|
||||||
let authorized_user = self.user.map(crate::is_authorized_user).unwrap_or(false);
|
|
||||||
|
|
||||||
if user.is_none() {
|
if self.authed_as.is_empty() {
|
||||||
for filter in filters.iter() {
|
for filter in filters.iter() {
|
||||||
// If any DM kinds were requested, complain.
|
// If any DM kinds were requested, complain.
|
||||||
// But if NO kinds were requested, we will just silently not return DMs (elsewhere)
|
// But if NO kinds were requested, we will just silently not return DMs (elsewhere)
|
||||||
@ -163,7 +162,7 @@ impl WebSocketService {
|
|||||||
|
|
||||||
for filter in filters.iter() {
|
for filter in filters.iter() {
|
||||||
let screen = |event: &Event| -> ScreenResult {
|
let screen = |event: &Event| -> ScreenResult {
|
||||||
let event_flags = event_flags(event, &user);
|
let event_flags = event_flags(event, self.authed_as.as_slice());
|
||||||
screen_outgoing_event(event, &event_flags, authorized_user)
|
screen_outgoing_event(event, &event_flags, authorized_user)
|
||||||
};
|
};
|
||||||
let (filter_events, was_redacted) = {
|
let (filter_events, was_redacted) = {
|
||||||
@ -317,13 +316,12 @@ impl WebSocketService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn event_inner(&mut self) -> Result<(), Error> {
|
async fn event_inner(&mut self) -> Result<(), Error> {
|
||||||
let user = self.user;
|
let authorized_user = self.authed_as.iter().any(|u| crate::is_authorized_user(*u));
|
||||||
let authorized_user = self.user.map(crate::is_authorized_user).unwrap_or(false);
|
|
||||||
|
|
||||||
// Delineate the event back out of the session buffer
|
// Delineate the event back out of the session buffer
|
||||||
let event = unsafe { Event::delineate(&self.buffer)? };
|
let event = unsafe { Event::delineate(&self.buffer)? };
|
||||||
|
|
||||||
let event_flags = event_flags(event, &user);
|
let event_flags = event_flags(event, self.authed_as.as_slice());
|
||||||
|
|
||||||
if GLOBALS.config.read().verify_events {
|
if GLOBALS.config.read().verify_events {
|
||||||
// Verify the event is valid (id is hash, signature is valid)
|
// Verify the event is valid (id is hash, signature is valid)
|
||||||
@ -347,10 +345,10 @@ impl WebSocketService {
|
|||||||
|
|
||||||
// Screen the event to see if we are willing to accept it
|
// Screen the event to see if we are willing to accept it
|
||||||
if !screen_incoming_event(event, event_flags, authorized_user).await? {
|
if !screen_incoming_event(event, event_flags, authorized_user).await? {
|
||||||
if self.user.is_some() {
|
if self.authed_as.is_empty() {
|
||||||
return Err(ChorusError::Restricted.into());
|
|
||||||
} else {
|
|
||||||
return Err(ChorusError::AuthRequired.into());
|
return Err(ChorusError::AuthRequired.into());
|
||||||
|
} else {
|
||||||
|
return Err(ChorusError::Restricted.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -463,7 +461,7 @@ impl WebSocketService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// They are now authenticated
|
// They are now authenticated
|
||||||
self.user = Some(event.pubkey());
|
self.authed_as.push(event.pubkey());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -542,13 +540,12 @@ impl WebSocketService {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = self.user;
|
let authorized_user = self.authed_as.iter().any(|u| crate::is_authorized_user(*u));
|
||||||
let authorized_user = self.user.map(crate::is_authorized_user).unwrap_or(false);
|
|
||||||
|
|
||||||
// Find all matching events
|
// Find all matching events
|
||||||
let mut events: Vec<&Event> = Vec::new();
|
let mut events: Vec<&Event> = Vec::new();
|
||||||
let screen = |event: &Event| -> ScreenResult {
|
let screen = |event: &Event| -> ScreenResult {
|
||||||
let event_flags = event_flags(event, &user);
|
let event_flags = event_flags(event, self.authed_as.as_slice());
|
||||||
screen_outgoing_event(event, &event_flags, authorized_user)
|
screen_outgoing_event(event, &event_flags, authorized_user)
|
||||||
};
|
};
|
||||||
let (filter_events, _redacted) = {
|
let (filter_events, _redacted) = {
|
||||||
@ -871,13 +868,10 @@ pub struct EventFlags {
|
|||||||
pub tags_current_user: bool,
|
pub tags_current_user: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn event_flags(event: &Event, user: &Option<Pubkey>) -> EventFlags {
|
pub fn event_flags(event: &Event, authed_as: &[Pubkey]) -> EventFlags {
|
||||||
let author_is_an_authorized_user = crate::is_authorized_user(event.pubkey());
|
let author_is_an_authorized_user = crate::is_authorized_user(event.pubkey());
|
||||||
|
|
||||||
let author_is_current_user = match user {
|
let author_is_current_user = authed_as.iter().any(|u| *u == event.pubkey());
|
||||||
None => false,
|
|
||||||
Some(pk) => event.pubkey() == *pk,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut tags_an_authorized_user = false;
|
let mut tags_an_authorized_user = false;
|
||||||
let mut tags_current_user = false;
|
let mut tags_current_user = false;
|
||||||
@ -887,10 +881,8 @@ pub fn event_flags(event: &Event, user: &Option<Pubkey>) -> EventFlags {
|
|||||||
if let Some(b"p") = tag.next() {
|
if let Some(b"p") = tag.next() {
|
||||||
if let Some(value) = tag.next() {
|
if let Some(value) = tag.next() {
|
||||||
if let Ok(tagged_pk) = Pubkey::read_hex(value) {
|
if let Ok(tagged_pk) = Pubkey::read_hex(value) {
|
||||||
if let Some(current_user) = user {
|
if authed_as.contains(&tagged_pk) {
|
||||||
if *current_user == tagged_pk {
|
tags_current_user = true;
|
||||||
tags_current_user = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if crate::is_authorized_user(tagged_pk) {
|
if crate::is_authorized_user(tagged_pk) {
|
||||||
|
|||||||
@ -53,6 +53,36 @@ pub async fn serve_http(
|
|||||||
|
|
||||||
let uri = request.uri().to_owned();
|
let uri = request.uri().to_owned();
|
||||||
|
|
||||||
|
if p == "/privacy-policy" {
|
||||||
|
let config = &*GLOBALS.config.read();
|
||||||
|
if let Some(pp) = &config.privacy_policy {
|
||||||
|
let response = Response::builder()
|
||||||
|
.header("Access-Control-Allow-Origin", "*")
|
||||||
|
.header("Access-Control-Allow-Headers", "Authorization, *")
|
||||||
|
.header("Access-Control-Allow-Methods", "*")
|
||||||
|
.header("Allow", "OPTIONS, GET, HEAD")
|
||||||
|
.header("Content-Type", "text/plain")
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Full::new(pp.clone().into()).map_err(|e| e.into()).boxed())?;
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p == "/terms-of-service" {
|
||||||
|
let config = &*GLOBALS.config.read();
|
||||||
|
if let Some(tos) = &config.terms_of_service {
|
||||||
|
let response = Response::builder()
|
||||||
|
.header("Access-Control-Allow-Origin", "*")
|
||||||
|
.header("Access-Control-Allow-Headers", "Authorization, *")
|
||||||
|
.header("Access-Control-Allow-Methods", "*")
|
||||||
|
.header("Allow", "OPTIONS, GET, HEAD")
|
||||||
|
.header("Content-Type", "text/plain")
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Full::new(tos.clone().into()).map_err(|e| e.into()).boxed())?;
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Try blossom if enabled
|
// Try blossom if enabled
|
||||||
if GLOBALS.config.read().blossom_directory.is_some() {
|
if GLOBALS.config.read().blossom_directory.is_some() {
|
||||||
match blossom::handle(request).await {
|
match blossom::handle(request).await {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ use crate::ip::HashedPeer;
|
|||||||
use http_body_util::combinators::BoxBody;
|
use http_body_util::combinators::BoxBody;
|
||||||
use http_body_util::{BodyExt, Full};
|
use http_body_util::{BodyExt, Full};
|
||||||
use hyper::body::Bytes;
|
use hyper::body::Bytes;
|
||||||
|
use hyper::http::uri::Uri;
|
||||||
use hyper::{Response, StatusCode};
|
use hyper::{Response, StatusCode};
|
||||||
|
|
||||||
pub async fn serve_nip11(peer: HashedPeer) -> Result<Response<BoxBody<Bytes, Error>>, Error> {
|
pub async fn serve_nip11(peer: HashedPeer) -> Result<Response<BoxBody<Bytes, Error>>, Error> {
|
||||||
@ -80,18 +81,18 @@ fn build_rid(config: &Config) -> String {
|
|||||||
rid.push_str(description);
|
rid.push_str(description);
|
||||||
rid.push('\"');
|
rid.push('\"');
|
||||||
}
|
}
|
||||||
|
if let Some(banner_url) = &config.banner_url {
|
||||||
|
rid.push(',');
|
||||||
|
rid.push_str("\"banner\":\"");
|
||||||
|
rid.push_str(banner_url);
|
||||||
|
rid.push('\"');
|
||||||
|
}
|
||||||
if let Some(icon_url) = &config.icon_url {
|
if let Some(icon_url) = &config.icon_url {
|
||||||
rid.push(',');
|
rid.push(',');
|
||||||
rid.push_str("\"icon\":\"");
|
rid.push_str("\"icon\":\"");
|
||||||
rid.push_str(icon_url);
|
rid.push_str(icon_url);
|
||||||
rid.push('\"');
|
rid.push('\"');
|
||||||
}
|
}
|
||||||
if let Some(contact) = &config.contact {
|
|
||||||
rid.push(',');
|
|
||||||
rid.push_str("\"contact\":\"");
|
|
||||||
rid.push_str(contact);
|
|
||||||
rid.push('\"');
|
|
||||||
}
|
|
||||||
if let Some(pubkey) = &config.contact_public_key {
|
if let Some(pubkey) = &config.contact_public_key {
|
||||||
let mut pkh: [u8; 64] = [0; 64];
|
let mut pkh: [u8; 64] = [0; 64];
|
||||||
pubkey.write_hex(&mut pkh).unwrap();
|
pubkey.write_hex(&mut pkh).unwrap();
|
||||||
@ -100,6 +101,44 @@ fn build_rid(config: &Config) -> String {
|
|||||||
rid.push_str(unsafe { std::str::from_utf8_unchecked(pkh.as_slice()) });
|
rid.push_str(unsafe { std::str::from_utf8_unchecked(pkh.as_slice()) });
|
||||||
rid.push('\"');
|
rid.push('\"');
|
||||||
}
|
}
|
||||||
|
if let Some(contact) = &config.contact {
|
||||||
|
rid.push(',');
|
||||||
|
rid.push_str("\"contact\":\"");
|
||||||
|
rid.push_str(contact);
|
||||||
|
rid.push('\"');
|
||||||
|
}
|
||||||
|
if config.privacy_policy.is_some() {
|
||||||
|
rid.push(',');
|
||||||
|
rid.push_str("\"privacy_policy\":\"");
|
||||||
|
let url = match config.uri_parts(
|
||||||
|
Uri::from_static("https://authority-will-be-replaced/privacy-policy"),
|
||||||
|
true,
|
||||||
|
) {
|
||||||
|
Ok(parts) => match Uri::from_parts(parts) {
|
||||||
|
Ok(uri) => format!("{}", uri),
|
||||||
|
Err(_) => "".to_owned(),
|
||||||
|
},
|
||||||
|
Err(_) => "".to_owned(),
|
||||||
|
};
|
||||||
|
rid.push_str(&url);
|
||||||
|
rid.push('\"');
|
||||||
|
}
|
||||||
|
if config.terms_of_service.is_some() {
|
||||||
|
rid.push(',');
|
||||||
|
rid.push_str("\"terms_of_service\":\"");
|
||||||
|
let url = match config.uri_parts(
|
||||||
|
Uri::from_static("https://authority-will-be-replaced/terms-of-service"),
|
||||||
|
true,
|
||||||
|
) {
|
||||||
|
Ok(parts) => match Uri::from_parts(parts) {
|
||||||
|
Ok(uri) => format!("{}", uri),
|
||||||
|
Err(_) => "".to_owned(),
|
||||||
|
},
|
||||||
|
Err(_) => "".to_owned(),
|
||||||
|
};
|
||||||
|
rid.push_str(&url);
|
||||||
|
rid.push('\"');
|
||||||
|
}
|
||||||
|
|
||||||
// Limitation
|
// Limitation
|
||||||
rid.push(',');
|
rid.push(',');
|
||||||
|
|||||||
@ -18,6 +18,12 @@ then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ADD ADMIN AS A USER ------------
|
||||||
|
|
||||||
|
echo "Adding user..."
|
||||||
|
PUBKEY=12bb541d03bfc3cab0f4a8e4db28947f60faae6fca4e315eb27f809c6eff9a0b
|
||||||
|
../target/release/chorus_cmd ./config.toml add_user $PUBKEY 0
|
||||||
|
|
||||||
# UPLOAD TEST ------------
|
# UPLOAD TEST ------------
|
||||||
|
|
||||||
FILE="./Example.png"
|
FILE="./Example.png"
|
||||||
|
|||||||
3
test_with_relay_tester/.gitignore
vendored
Normal file
3
test_with_relay_tester/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
data
|
||||||
|
relay-tester
|
||||||
|
|
||||||
12
test_with_relay_tester/README.md
Normal file
12
test_with_relay_tester/README.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Testing Chorus with relay-tester
|
||||||
|
|
||||||
|
First, git clone https://github.com/mikedilger/relay-tester and build that project
|
||||||
|
(cargo build --release).
|
||||||
|
|
||||||
|
Then copy that target/release/relay-tester binary into this directory.
|
||||||
|
|
||||||
|
Then run from two different shells, in this order:
|
||||||
|
|
||||||
|
shell1: ./test_chorus.sh
|
||||||
|
|
||||||
|
shell2: ./run_relay_tester.sh
|
||||||
12
test_with_relay_tester/run_relay_tester.sh
Executable file
12
test_with_relay_tester/run_relay_tester.sh
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ ! -x relay-tester ] ; then
|
||||||
|
echo "You must build https://github.com/mikedilge/relay-tester and copy the "
|
||||||
|
echo "resultant target/release/relay-tester binary into this directory."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
./relay-tester \
|
||||||
|
ws://localhost:8080/ \
|
||||||
|
nsec16xfd467kyd3xpu9x5u4933u00v73xrl0jyq9rk5ktd9t2j38k20qtwxuj3 \
|
||||||
|
nsec1l50yuf6uxm2l5qxm87fkm56z3m7g88jnfy5s6az5wscxpu5l2yqq6qwk88
|
||||||
10
test_with_relay_tester/test_chorus.sh
Executable file
10
test_with_relay_tester/test_chorus.sh
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Use ws://localhost:8080/ as the relay url"
|
||||||
|
|
||||||
|
cargo build --release
|
||||||
|
rm -rf ./data/
|
||||||
|
../target/release/chorus_init ./test_chorus.toml
|
||||||
|
../target/release/chorus_cmd ./test_chorus.toml add_user de16d3ed2d5ceb91d33e39dbe30585164e0c19f3f2e2a5b121def086b447a2e5 0
|
||||||
|
../target/release/chorus_cmd ./test_chorus.toml add_user 35d6bbcf17fc31a9c4f7a2f68aa40ad32c8f9de1ae77505dc5eb3722d8b2987d 0
|
||||||
|
../target/release/chorus ./test_chorus.toml
|
||||||
40
test_with_relay_tester/test_chorus.toml
Normal file
40
test_with_relay_tester/test_chorus.toml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# See contrib/chorus.toml for a documented config file
|
||||||
|
|
||||||
|
data_directory = "./data"
|
||||||
|
ip_address = "127.0.0.1"
|
||||||
|
port = 8080
|
||||||
|
hostname = "localhost"
|
||||||
|
chorus_is_behind_a_proxy = false
|
||||||
|
use_tls = false
|
||||||
|
certchain_pem_path = "tls/fullchain.pem"
|
||||||
|
key_pem_path = "tls/privkey.pem"
|
||||||
|
name = "Chorus Sample"
|
||||||
|
description = "A sample run of the Chorus relay"
|
||||||
|
#icon_url =
|
||||||
|
open_relay = false
|
||||||
|
admin_hex_keys = [
|
||||||
|
"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49",
|
||||||
|
|
||||||
|
# npub1mctd8mfdtn4er5e788d7xpv9ze8qcx0n7t32tvfpmmcgddz85tjsuyxe7z
|
||||||
|
"de16d3ed2d5ceb91d33e39dbe30585164e0c19f3f2e2a5b121def086b447a2e5",
|
||||||
|
|
||||||
|
# npub1xhtthnchlsc6n38h5tmg4fq26vkgl80p4em4qhw9avmj9k9jnp7sm78ql6
|
||||||
|
"35d6bbcf17fc31a9c4f7a2f68aa40ad32c8f9de1ae77505dc5eb3722d8b2987d"
|
||||||
|
]
|
||||||
|
verify_events = true
|
||||||
|
allow_scraping = false
|
||||||
|
allow_scrape_if_limited_to = 100
|
||||||
|
allow_scrape_if_max_seconds = 7200
|
||||||
|
max_subscriptions = 128
|
||||||
|
serve_ephemeral = true
|
||||||
|
serve_relay_lists = true
|
||||||
|
server_log_level = "Info"
|
||||||
|
library_log_level = "Info"
|
||||||
|
client_log_level = "Debug"
|
||||||
|
enable_ip_blocking = false
|
||||||
|
minimum_ban_seconds = 1
|
||||||
|
timeout_seconds = 60
|
||||||
|
max_connections_per_ip = 5
|
||||||
|
throttling_bytes_per_second = 131072
|
||||||
|
throttling_burst = 4194304
|
||||||
|
enable_negentropy = true
|
||||||
Loading…
x
Reference in New Issue
Block a user