Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ futures-util = "0.3"
gcra = "0.4"
hex = "0.4"
hex-literal = "0.4.1"
http = "0.2.12"
im = "15.1.0"
integer-encoding = { version = "3.0.3", default-features = false }
jsonrpc-v2 = { version = "0.11", default-features = false, features = [
Expand Down Expand Up @@ -153,6 +154,7 @@ tokio = { version = "1", features = [
"io-std",
"sync",
] }
tower-http = { version = "0.4.0", features = ["cors"] }
tokio-stream = "0.1.14"
tokio-util = { version = "0.7.8", features = ["compat"] }
tokio-tungstenite = { version = "0.18.0", features = ["native-tls"] }
Expand Down
15 changes: 11 additions & 4 deletions fendermint/app/config/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,17 @@ host = "127.0.0.1"
# The default port where the Prometheus exporter makes the metrics available.
port = 9185

[eth.cors]
# A list of origins a cross-domain request can be executed from
# Default value '[]' disables cors support
# Use '["*"]' to allow any origin
allowed_origins = []
# A list of methods the client is allowed to use with cross-domain requests
# Suggested methods if allowing origins: "GET", "OPTIONS", "HEAD", "POST"
allowed_methods = []
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left these empty by default because there is no way to override them to be empty using an env var (empty strings are ignored by the config list parser).

# A list of non-simple headers the client is allowed to use with cross-domain requests
# Suggested headers if allowing origins: "Accept", "Authorization", "Content-Type", "Origin"
allowed_headers = []

# IPLD Resolver Configuration
[resolver]
Expand All @@ -164,7 +175,6 @@ local_key = "keys/network.sk"
# so we can derive a name using the rootnet ID and use this as an override.
network_name = ""


# Peer Discovery
[resolver.discovery]
# Bootstrap node addresses for peer discovery, or the entire list for a static network
Expand All @@ -179,7 +189,6 @@ target_connections = 50
# Option to disable Kademlia, for example in a fixed static network.
enable_kademlia = true


# IPC Subnet Membership
[resolver.membership]
# User defined list of subnets which will never be pruned from the cache.
Expand Down Expand Up @@ -224,7 +233,6 @@ max_peers_per_query = 5
# consumer gets an error because it's falling behind.
event_buffer_capacity = 100


# Serving Content
[resolver.content]
# Number of bytes that can be consumed by remote peers in a time period. 0 means no limit.
Expand All @@ -247,7 +255,6 @@ vote_interval = 1
# pausing the syncer, preventing new events to trigger votes.
vote_timeout = 60


# # Setting which are only allowed if the `--network` CLI parameter is `testnet`.
# [testing]

Expand Down
3 changes: 2 additions & 1 deletion fendermint/app/settings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ serde_json = { workspace = true }
serde_with = { workspace = true }
serial_test = { workspace = true }
tendermint-rpc = { workspace = true }
tower-http = { workspace = true }
tracing = { workspace = true }

fvm_shared = { workspace = true }
fvm_ipld_encoding = { workspace = true }
ipc-api = { workspace = true }
ipc-provider = { workspace = true }
tracing = { workspace = true }

fendermint_vm_encoding = { path = "../../vm/encoding" }
fendermint_vm_topdown = { path = "../../vm/topdown" }
16 changes: 16 additions & 0 deletions fendermint/app/settings/src/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
// SPDX-License-Identifier: Apache-2.0, MIT

use fvm_shared::econ::TokenAmount;
use ipc_provider::config::deserialize::{
deserialize_cors_headers, deserialize_cors_methods, deserialize_cors_origins,
};
use serde::Deserialize;
use serde_with::{serde_as, DurationSeconds};
use std::time::Duration;
use tower_http::cors::{AllowHeaders, AllowMethods, AllowOrigin};

use crate::{IsHumanReadable, MetricsSettings, SocketAddress};

Expand All @@ -19,6 +23,7 @@ pub struct EthSettings {
pub gas: GasOpt,
pub max_nonce_gap: u64,
pub metrics: MetricsSettings,
pub cors: CorsOpt,
}

#[serde_as]
Expand All @@ -30,3 +35,14 @@ pub struct GasOpt {
pub num_blocks_max_prio_fee: u64,
pub max_fee_hist_size: u64,
}

#[serde_as]
#[derive(Debug, Clone, Deserialize)]
pub struct CorsOpt {
#[serde(deserialize_with = "deserialize_cors_origins")]
pub allowed_origins: AllowOrigin,
#[serde(deserialize_with = "deserialize_cors_methods")]
pub allowed_methods: AllowMethods,
#[serde(deserialize_with = "deserialize_cors_headers")]
pub allowed_headers: AllowHeaders,
}
96 changes: 89 additions & 7 deletions fendermint/app/settings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ impl Display for SocketAddress {
}
}

impl std::net::ToSocketAddrs for SocketAddress {
type Iter = <String as std::net::ToSocketAddrs>::Iter;
impl ToSocketAddrs for SocketAddress {
type Iter = <String as ToSocketAddrs>::Iter;

fn to_socket_addrs(&self) -> std::io::Result<Self::Iter> {
self.to_string().to_socket_addrs()
}
}

impl TryInto<std::net::SocketAddr> for SocketAddress {
impl TryInto<SocketAddr> for SocketAddress {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some cleanups... can revert if annoying.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to prefer using fully-qualified type/trait names in trait implementations when they're stdlib or 3rd party types, to signal clearly that they're external. Will take the liberty to revert this in your branch.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense! My IDE shows a warning if a trait/struct has a top level import, but the fully qualified path is used in the same file. That's what happened in this case.

type Error = std::io::Error;

fn try_into(self) -> Result<SocketAddr, Self::Error> {
Expand Down Expand Up @@ -328,7 +328,10 @@ impl Settings {
.list_separator(",") // need to list keys explicitly below otherwise it can't pase simple `String` type
.with_list_parse_key("resolver.connection.external_addresses")
.with_list_parse_key("resolver.discovery.static_addresses")
.with_list_parse_key("resolver.membership.static_subnets"),
.with_list_parse_key("resolver.membership.static_subnets")
.with_list_parse_key("eth.cors.allowed_origins")
.with_list_parse_key("eth.cors.allowed_methods")
.with_list_parse_key("eth.cors.allowed_headers"),
))
// Set the home directory based on what was passed to the CLI,
// so everything in the config can be relative to it.
Expand Down Expand Up @@ -381,7 +384,7 @@ mod tests {

use crate::DbCompaction;

use super::Settings;
use super::{ConfigError, Settings};

fn try_parse_config(run_mode: &str) -> Result<Settings, config::ConfigError> {
let current_dir = PathBuf::from(".");
Expand Down Expand Up @@ -422,21 +425,41 @@ mod tests {
let settings = with_env_vars(vec![
("FM_RESOLVER__CONNECTION__EXTERNAL_ADDRESSES", "/ip4/198.51.100.0/tcp/4242/p2p/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N,/ip6/2604:1380:2000:7a00::1/udp/4001/quic/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb"),
("FM_RESOLVER__DISCOVERY__STATIC_ADDRESSES", "/ip4/198.51.100.1/tcp/4242/p2p/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N,/ip6/2604:1380:2000:7a00::2/udp/4001/quic/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb"),
("FM_RESOLVER__MEMBERSHIP__STATIC_SUBNETS", "/r314/f410fijl3evsntewwhqxy6cx5ijdq5qp5cjlocbgzgey,/r314/f410fwplxlims2wnigaha2gofgktue7hiusmttwridkq"),
("FM_ETH__CORS__ALLOWED_ORIGINS", "https://example.com,https://www.example.org"),
("FM_ETH__CORS__ALLOWED_METHODS", "GET,POST"),
("FM_ETH__CORS__ALLOWED_HEADERS", "Accept,Content-Type"),
// Set a normal string key as well to make sure we have configured the library correctly and it doesn't try to parse everything as a list.
("FM_RESOLVER__NETWORK__NETWORK_NAME", "test"),
], || try_parse_config("")).unwrap();

assert_eq!(settings.resolver.discovery.static_addresses.len(), 2);
assert_eq!(settings.resolver.connection.external_addresses.len(), 2);
assert_eq!(settings.resolver.discovery.static_addresses.len(), 2);
assert_eq!(settings.resolver.membership.static_subnets.len(), 2);
assert_eq!(
format!("{:?}", settings.eth.cors.allowed_origins),
"List([\"https://example.com\", \"https://www.example.org\"])"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugly, but no other way of testing this... there isn't a public API on AllowedOrigin to list its members.

);
assert_eq!(
format!("{:?}", settings.eth.cors.allowed_methods),
"Const(Some(\"GET,POST\"))"
);
assert_eq!(
format!("{:?}", settings.eth.cors.allowed_headers),
"Const(Some(\"accept,content-type\"))"
);
}

#[test]
fn parse_empty_comma_separated() {
let settings = with_env_vars(
vec![
("FM_RESOLVER__DISCOVERY__STATIC_ADDRESSES", ""),
("FM_RESOLVER__CONNECTION__EXTERNAL_ADDRESSES", ""),
("FM_RESOLVER__DISCOVERY__STATIC_ADDRESSES", ""),
("FM_RESOLVER__MEMBERSHIP__STATIC_SUBNETS", ""),
("FM_ETH__CORS__ALLOWED_ORIGINS", ""),
("FM_ETH__CORS__ALLOWED_METHODS", ""),
("FM_ETH__CORS__ALLOWED_HEADERS", ""),
],
|| try_parse_config(""),
)
Expand All @@ -445,6 +468,18 @@ mod tests {
assert_eq!(settings.resolver.connection.external_addresses.len(), 0);
assert_eq!(settings.resolver.discovery.static_addresses.len(), 0);
assert_eq!(settings.resolver.membership.static_subnets.len(), 0);
assert_eq!(
format!("{:?}", settings.eth.cors.allowed_origins),
"List([])"
);
assert_eq!(
format!("{:?}", settings.eth.cors.allowed_methods),
"Const(None)"
);
assert_eq!(
format!("{:?}", settings.eth.cors.allowed_headers),
"Const(None)"
);
}

#[test]
Expand All @@ -471,4 +506,51 @@ mod tests {
multiaddr!(Dns4("bar.ai"), Tcp(5678u16))
);
}

#[test]
fn parse_cors_origins_variants() {
// relative URL without a base
let settings = with_env_vars(
vec![("FM_ETH__CORS__ALLOWED_ORIGINS", "example.com")],
|| try_parse_config(""),
);
assert!(
matches!(settings, Err(ConfigError::Message(ref msg)) if msg == "relative URL without a base")
);

// opaque origin
let settings = with_env_vars(
vec![(
"FM_ETH__CORS__ALLOWED_ORIGINS",
"javascript:console.log(\"invalid origin\")",
)],
|| try_parse_config(""),
);
assert!(
matches!(settings, Err(ConfigError::Message(ref msg)) if msg == "opaque origins are not allowed")
);

// Allow all with "*"
let settings = with_env_vars(vec![("FM_ETH__CORS__ALLOWED_ORIGINS", "*")], || {
try_parse_config("")
});
assert!(settings.is_ok());

// IPv4
let settings = with_env_vars(
vec![("FM_ETH__CORS__ALLOWED_ORIGINS", "http://192.0.2.1:1234")],
|| try_parse_config(""),
);
assert!(settings.is_ok());

// IPv6
let settings = with_env_vars(
vec![(
"FM_ETH__CORS__ALLOWED_ORIGINS",
"http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:1234",
)],
|| try_parse_config(""),
);
assert!(settings.is_ok());
}
}
6 changes: 6 additions & 0 deletions fendermint/app/src/cmd/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,19 @@ async fn run(settings: EthSettings, client: HybridClient) -> anyhow::Result<()>
num_blocks_max_prio_fee: settings.gas.num_blocks_max_prio_fee,
max_fee_hist_size: settings.gas.max_fee_hist_size,
};
let cors = fendermint_eth_api::CorsOpt {
allowed_origins: settings.cors.allowed_origins,
allowed_methods: settings.cors.allowed_methods,
allowed_headers: settings.cors.allowed_headers,
};
fendermint_eth_api::listen(
settings.listen,
client,
settings.filter_timeout,
settings.cache_capacity,
settings.max_nonce_gap,
gas,
cors,
)
.await
}
Loading