Skip to content

Commit 100dc08

Browse files
committed
add user agent in http requests
1 parent a93cafe commit 100dc08

File tree

8 files changed

+168
-15
lines changed

8 files changed

+168
-15
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

beacon_node/builder_client/src/lib.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ use eth2::types::{
77
};
88
use eth2::types::{FullPayloadContents, SignedBlindedBeaconBlock};
99
use eth2::{
10-
CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE_HEADER,
11-
SSZ_CONTENT_TYPE_HEADER, StatusCode, ok_or_error,
10+
CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, HttpClientBuilderWithUserAgent,
11+
JSON_CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER, StatusCode, ok_or_error,
1212
};
1313
use reqwest::header::{ACCEPT, HeaderMap, HeaderValue};
1414
use reqwest::{IntoUrl, Response};
@@ -26,9 +26,6 @@ pub const DEFAULT_TIMEOUT_MILLIS: u64 = 15000;
2626
/// This timeout is in accordance with v0.2.0 of the [builder specs](https://github.com/flashbots/mev-boost/pull/20).
2727
pub const DEFAULT_GET_HEADER_TIMEOUT_MILLIS: u64 = 1000;
2828

29-
/// Default user agent for HTTP requests.
30-
pub const DEFAULT_USER_AGENT: &str = lighthouse_version::VERSION;
31-
3229
/// The value we set on the `ACCEPT` http header to indicate a preference for ssz response.
3330
pub const PREFERENCE_ACCEPT_VALUE: &str = "application/octet-stream;q=1.0,application/json;q=0.9";
3431
/// Only accept json responses.
@@ -77,8 +74,8 @@ impl BuilderHttpClient {
7774
builder_header_timeout: Option<Duration>,
7875
disable_ssz: bool,
7976
) -> Result<Self, Error> {
80-
let user_agent = user_agent.unwrap_or(DEFAULT_USER_AGENT.to_string());
81-
let client = reqwest::Client::builder().user_agent(&user_agent).build()?;
77+
let user_agent = user_agent.unwrap_or(lighthouse_version::user_agent());
78+
let client = HttpClientBuilderWithUserAgent::new(Some(user_agent.clone())).build()?;
8279
Ok(Self {
8380
client,
8481
server,

common/eth2/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ ethereum_ssz_derive = { workspace = true }
1919
futures = { workspace = true }
2020
futures-util = "0.3.8"
2121
libp2p-identity = { version = "0.2", features = ["peerid"] }
22+
lighthouse_version = { workspace = true }
2223
mediatype = "0.19.13"
2324
multiaddr = "0.18.2"
2425
pretty_reqwest_error = { workspace = true }
@@ -37,3 +38,5 @@ zeroize = { workspace = true }
3738

3839
[dev-dependencies]
3940
tokio = { workspace = true }
41+
mockito = { workspace = true }
42+
regex = { workspace = true }

common/eth2/src/lib.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ use derivative::Derivative;
2121
use futures::Stream;
2222
use futures_util::StreamExt;
2323
use libp2p_identity::PeerId;
24+
use lighthouse_version;
2425
use pretty_reqwest_error::PrettyReqwestError;
25-
pub use reqwest;
26+
use reqwest;
2627
use reqwest::{
2728
Body, IntoUrl, RequestBuilder, Response,
2829
header::{HeaderMap, HeaderValue},
@@ -237,8 +238,13 @@ impl AsRef<str> for BeaconNodeHttpClient {
237238

238239
impl BeaconNodeHttpClient {
239240
pub fn new(server: SensitiveUrl, timeouts: Timeouts) -> Self {
241+
let client = reqwest::ClientBuilder::new()
242+
.user_agent(lighthouse_version::user_agent())
243+
.build()
244+
.unwrap_or_else(|_| reqwest::Client::new());
245+
240246
Self {
241-
client: reqwest::Client::new(),
247+
client,
242248
server,
243249
timeouts,
244250
}
@@ -2903,3 +2909,13 @@ pub async fn ok_or_error(response: Response) -> Result<Response, Error> {
29032909
Err(Error::StatusCode(status))
29042910
}
29052911
}
2912+
2913+
/// A wrapper around `reqwest::ClientBuilder` which adds a user agent for client identification
2914+
pub struct HttpClientBuilderWithUserAgent;
2915+
2916+
impl HttpClientBuilderWithUserAgent {
2917+
pub fn new(user_agent: Option<String>) -> reqwest::ClientBuilder {
2918+
let user_agent = user_agent.unwrap_or(lighthouse_version::user_agent());
2919+
reqwest::ClientBuilder::new().user_agent(user_agent)
2920+
}
2921+
}

common/eth2/src/lighthouse_vc/http_client.rs

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use super::types::*;
22
use crate::Error;
3+
use lighthouse_version;
34
use reqwest::{
45
IntoUrl,
56
header::{HeaderMap, HeaderValue},
@@ -45,8 +46,13 @@ impl Display for AuthorizationHeader {
4546
impl ValidatorClientHttpClient {
4647
/// Create a new client pre-initialised with an API token.
4748
pub fn new(server: SensitiveUrl, secret: String) -> Result<Self, Error> {
49+
let client = reqwest::ClientBuilder::new()
50+
.user_agent(lighthouse_version::user_agent())
51+
.build()
52+
.unwrap_or_else(|_| reqwest::Client::new());
53+
4854
Ok(Self {
49-
client: reqwest::Client::new(),
55+
client,
5056
server,
5157
api_token: Some(secret.into()),
5258
authorization_header: AuthorizationHeader::Bearer,
@@ -57,8 +63,13 @@ impl ValidatorClientHttpClient {
5763
///
5864
/// A token can be fetched by using `self.get_auth`, and then reading the token from disk.
5965
pub fn new_unauthenticated(server: SensitiveUrl) -> Result<Self, Error> {
66+
let client = reqwest::ClientBuilder::new()
67+
.user_agent(lighthouse_version::user_agent())
68+
.build()
69+
.unwrap_or_else(|_| reqwest::Client::new());
70+
6071
Ok(Self {
61-
client: reqwest::Client::new(),
72+
client,
6273
server,
6374
api_token: None,
6475
authorization_header: AuthorizationHeader::Omit,
@@ -698,3 +709,87 @@ async fn ok_or_error(response: Response) -> Result<Response, Error> {
698709
Err(Error::StatusCode(status))
699710
}
700711
}
712+
713+
#[cfg(test)]
714+
mod tests {
715+
use super::*;
716+
use mockito::{Matcher, Server};
717+
use std::str::FromStr;
718+
719+
#[test]
720+
fn test_validator_client_creation_with_user_agent() {
721+
let server = SensitiveUrl::parse("http://localhost:5062").unwrap();
722+
let secret = "test-secret".to_string();
723+
724+
// Test authenticated client
725+
let client = ValidatorClientHttpClient::new(server.clone(), secret.clone()).unwrap();
726+
assert!(client.api_token().is_some());
727+
assert_eq!(client.authorization_header, AuthorizationHeader::Bearer);
728+
729+
// Test unauthenticated client
730+
let unauth_client = ValidatorClientHttpClient::new_unauthenticated(server).unwrap();
731+
assert!(unauth_client.api_token().is_none());
732+
assert_eq!(unauth_client.authorization_header, AuthorizationHeader::Omit);
733+
}
734+
735+
#[tokio::test]
736+
async fn test_validator_client_user_agent_in_requests() {
737+
// Create mock server
738+
let mut server = Server::new_async().await;
739+
let expected_user_agent = lighthouse_version::user_agent();
740+
741+
// Mock the auth endpoint with user agent verification
742+
let auth_mock = server
743+
.mock("GET", "/lighthouse/auth")
744+
.match_header("user-agent", expected_user_agent.as_str())
745+
.with_status(200)
746+
.with_body(r#"{"token_path":"/tmp/test","api_token":"test-token"}"#)
747+
.create_async()
748+
.await;
749+
750+
// Create client
751+
let server_url = SensitiveUrl::parse(&server.url()).unwrap();
752+
let client = ValidatorClientHttpClient::new_unauthenticated(server_url).unwrap();
753+
754+
// Make request - this should include the user agent header
755+
let _result = client.get_auth().await;
756+
757+
// Verify the mock was called with correct user agent
758+
auth_mock.assert_async().await;
759+
}
760+
761+
#[tokio::test]
762+
async fn test_validator_client_user_agent_with_auth_token() {
763+
// Create mock server
764+
let mut server = Server::new_async().await;
765+
let expected_user_agent = lighthouse_version::user_agent();
766+
let auth_token = "test-bearer-token";
767+
768+
// Mock the keystores endpoint with both user agent and authorization verification
769+
let keystores_mock = server
770+
.mock("GET", "/eth/v1/keystores")
771+
.match_header("user-agent", expected_user_agent.as_str())
772+
.match_header("authorization", format!("Bearer {}", auth_token).as_str())
773+
.with_status(200)
774+
.with_body(r#"{"data":[]}"#)
775+
.create_async()
776+
.await;
777+
778+
// Create authenticated client
779+
let server_url = SensitiveUrl::parse(&server.url()).unwrap();
780+
let client = ValidatorClientHttpClient::new(server_url, auth_token.to_string()).unwrap();
781+
782+
// Make request - this should include both user agent and authorization headers
783+
let _result = client.get_keystores().await;
784+
785+
// Verify the mock was called with correct headers
786+
keystores_mock.assert_async().await;
787+
}
788+
789+
#[test]
790+
fn test_authorization_header_display() {
791+
assert_eq!(AuthorizationHeader::Omit.to_string(), "Omit");
792+
assert_eq!(AuthorizationHeader::Basic.to_string(), "Basic");
793+
assert_eq!(AuthorizationHeader::Bearer.to_string(), "Bearer");
794+
}
795+
}

common/lighthouse_version/src/lib.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ pub fn version_with_platform() -> String {
4848
format!("{}/{}-{}", VERSION, consts::ARCH, consts::OS)
4949
}
5050

51+
/// Returns `CLIENT_NAME/VERSION` used as default User-Agent for HTTP requests.
52+
///
53+
/// ## Example
54+
///
55+
/// `Lighthouse/v7.1.0-67da032+`
56+
pub fn user_agent() -> String {
57+
format!("{}", VERSION)
58+
}
59+
5160
/// Returns semantic versioning information only.
5261
///
5362
/// ## Example
@@ -91,4 +100,34 @@ mod test {
91100
version()
92101
);
93102
}
103+
104+
#[test]
105+
fn user_agent_formatting() {
106+
let ua = user_agent();
107+
108+
// User agent should match the VERSION format
109+
let re = Regex::new(
110+
r"^Lighthouse/v[0-9]+\.[0-9]+\.[0-9]+(-(rc|beta).[0-9])?(-[[:xdigit:]]{7})?\+?$",
111+
)
112+
.unwrap();
113+
114+
assert!(
115+
re.is_match(&ua),
116+
"user agent doesn't match expected format: {}",
117+
ua
118+
);
119+
120+
// User agent should be equal to VERSION
121+
assert_eq!(ua, VERSION, "user_agent() should return VERSION");
122+
}
123+
124+
#[test]
125+
fn user_agent_non_empty() {
126+
let ua = user_agent();
127+
assert!(!ua.is_empty(), "user agent should not be empty");
128+
assert!(
129+
ua.starts_with("Lighthouse/"),
130+
"user agent should start with 'Lighthouse/'"
131+
);
132+
}
94133
}

testing/node_test_rig/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
use beacon_node::ProductionBeaconNode;
66
use environment::RuntimeContext;
7-
use eth2::{BeaconNodeHttpClient, Timeouts, reqwest::ClientBuilder};
7+
use eth2::{BeaconNodeHttpClient, Timeouts, HttpClientBuilderWithUserAgent};
88
use sensitive_url::SensitiveUrl;
99
use std::path::PathBuf;
1010
use std::time::Duration;
@@ -81,7 +81,7 @@ impl<E: EthSpec> LocalBeaconNode<E> {
8181
format!("http://{}:{}", listen_addr.ip(), listen_addr.port()).as_str(),
8282
)
8383
.map_err(|e| format!("Unable to parse beacon node URL: {:?}", e))?;
84-
let beacon_node_http_client = ClientBuilder::new()
84+
let beacon_node_http_client = HttpClientBuilderWithUserAgent::new(None)
8585
.timeout(HTTP_TIMEOUT)
8686
.build()
8787
.map_err(|e| format!("Unable to build HTTP client: {:?}", e))?;

validator_client/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use beacon_node_fallback::{
1717
use clap::ArgMatches;
1818
use doppelganger_service::DoppelgangerService;
1919
use environment::RuntimeContext;
20-
use eth2::{BeaconNodeHttpClient, StatusCode, Timeouts, reqwest::ClientBuilder};
20+
use eth2::{BeaconNodeHttpClient, HttpClientBuilderWithUserAgent, StatusCode, Timeouts};
2121
use initialized_validators::Error::UnableToOpenVotingKeystore;
2222
use lighthouse_validator_store::LighthouseValidatorStore;
2323
use parking_lot::RwLock;
@@ -272,7 +272,7 @@ impl<E: EthSpec> ProductionValidatorClient<E> {
272272
let url = x.1;
273273
let slot_duration = Duration::from_secs(context.eth2_config.spec.seconds_per_slot);
274274

275-
let mut beacon_node_http_client_builder = ClientBuilder::new();
275+
let mut beacon_node_http_client_builder = HttpClientBuilderWithUserAgent::new(None);
276276

277277
// Add new custom root certificates if specified.
278278
if let Some(certificates) = &config.beacon_nodes_tls_certs {

0 commit comments

Comments
 (0)