// Copyright 2015-2020 Parity Technologies (UK) Ltd. // This file is part of OpenEthereum. // OpenEthereum is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // OpenEthereum is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with OpenEthereum. If not, see . #![warn(missing_docs)] //! Parse ethereum client ID strings and provide querying functionality use semver::Version; use std::fmt; /// Parity client string prefix const LEGACY_CLIENT_ID_PREFIX: &str = "Parity-Ethereum"; const CURRENT_CLIENT_ID_PREFIX: &str = "OpenEthereum"; lazy_static! { /// Parity versions starting from this will accept block bodies requests /// of 256 bodies static ref PARITY_CLIENT_LARGE_REQUESTS_VERSION: Version = Version::parse("2.4.0").unwrap(); } /// Description of the software version running in a peer /// according to https://github.com/ethereum/wiki/wiki/Client-Version-Strings /// This structure as it is represents the format used by Parity clients. Other /// vendors may provide additional fields. #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct ParityClientData { name: String, identity: Option, semver: Version, os: String, compiler: String, // Capability flags, should be calculated in constructor can_handle_large_requests: bool, } /// Accessor methods for ParityClientData. This will probably /// need to be abstracted away into a trait. impl ParityClientData { fn new( name: String, identity: Option, semver: Version, os: String, compiler: String, ) -> Self { // Flags logic let can_handle_large_requests = &semver >= &PARITY_CLIENT_LARGE_REQUESTS_VERSION; // Instantiate and return ParityClientData { name: name, identity: identity, semver: semver, os: os, compiler: compiler, can_handle_large_requests: can_handle_large_requests, } } fn name(&self) -> &str { self.name.as_str() } fn identity(&self) -> Option<&str> { self.identity.as_ref().map(String::as_str) } fn semver(&self) -> &Version { &self.semver } fn os(&self) -> &str { self.os.as_str() } fn compiler(&self) -> &str { self.compiler.as_str() } fn can_handle_large_requests(&self) -> bool { self.can_handle_large_requests } } /// Enum describing the version of the software running on a peer. #[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub enum ClientVersion { /// The peer runs software from parity and the string format is known ParityClient( /// The actual information fields: name, version, os, ... ParityClientData, ), /// The string ID is recognized as Parity but the overall format /// could not be parsed ParityUnknownFormat(String), /// Other software vendors than Parity Other(String), } impl Default for ClientVersion { fn default() -> Self { ClientVersion::Other("".to_owned()) } } /// Provide information about what a particular version of a /// peer software can do pub trait ClientCapabilities { /// Parity versions before PARITY_CLIENT_LARGE_REQUESTS_VERSION would not /// check the accumulated size of a packet when building a response to a /// GET_BLOCK_BODIES request. If the packet was larger than a given limit, /// instead of sending fewer blocks no packet would get sent at all. Query /// if this version can handle requests for a large number of block bodies. fn can_handle_large_requests(&self) -> bool; /// Service transactions are specific to parity and nethermind. Query if /// this version accepts them. fn accepts_service_transaction(&self) -> bool; } impl ClientCapabilities for ClientVersion { fn can_handle_large_requests(&self) -> bool { match self { ClientVersion::ParityClient(data) => data.can_handle_large_requests(), ClientVersion::ParityUnknownFormat(_) => false, // Play it safe ClientVersion::Other(_) => true, // As far as we know } } fn accepts_service_transaction(&self) -> bool { match self { ClientVersion::ParityClient(_) => true, ClientVersion::ParityUnknownFormat(_) => true, ClientVersion::Other(client_id) => is_nethermind(client_id), } } } fn is_parity(client_id: &str) -> bool { client_id.starts_with(LEGACY_CLIENT_ID_PREFIX) || client_id.starts_with(CURRENT_CLIENT_ID_PREFIX) } fn is_nethermind(client_id: &str) -> bool { client_id.starts_with("Nethermind") } /// Parse known parity formats. Recognizes either a short format with four fields /// or a long format which includes the same fields and an identity one. fn parse_parity_format(client_version: &str) -> Result { const PARITY_ID_STRING_MINIMUM_TOKENS: usize = 4; let tokens: Vec<&str> = client_version.split("/").collect(); if tokens.len() < PARITY_ID_STRING_MINIMUM_TOKENS { return Err(()); } let name = tokens[0]; let identity = if tokens.len() - 3 > 1 { Some(tokens[1..(tokens.len() - 3)].join("/")) } else { None }; let compiler = tokens[tokens.len() - 1]; let os = tokens[tokens.len() - 2]; // If version is in the right position and valid format return a valid // result. Otherwise return an error. get_number_from_version(tokens[tokens.len() - 3]) .and_then(|v| Version::parse(v).ok()) .map(|semver| { ParityClientData::new( name.to_owned(), identity, semver, os.to_owned(), compiler.to_owned(), ) }) .ok_or(()) } /// Parse a version string and return the corresponding /// ClientVersion. Only Parity clients are destructured right now, other /// strings will just get wrapped in a variant so that the information is /// not lost. /// The parsing for parity may still fail, in which case return a ParityUnknownFormat with /// the original version string. TryFrom would be a better trait to implement. impl From for ClientVersion where T: AsRef, { fn from(client_version: T) -> Self { let client_version_str: &str = client_version.as_ref(); if !is_parity(client_version_str) { return ClientVersion::Other(client_version_str.to_owned()); } if let Ok(data) = parse_parity_format(client_version_str) { ClientVersion::ParityClient(data) } else { ClientVersion::ParityUnknownFormat(client_version_str.to_owned()) } } } fn format_parity_version_string( client_version: &ParityClientData, f: &mut fmt::Formatter, ) -> std::fmt::Result { let name = client_version.name(); let semver = client_version.semver(); let os = client_version.os(); let compiler = client_version.compiler(); match client_version.identity() { None => write!(f, "{}/v{}/{}/{}", name, semver, os, compiler), Some(identity) => write!(f, "{}/{}/v{}/{}/{}", name, identity, semver, os, compiler), } } impl fmt::Display for ClientVersion { fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { match self { ClientVersion::ParityClient(data) => format_parity_version_string(data, f), ClientVersion::ParityUnknownFormat(id) => write!(f, "{}", id), ClientVersion::Other(id) => write!(f, "{}", id), } } } fn get_number_from_version(version: &str) -> Option<&str> { if version.starts_with("v") { return version.get(1..); } None } #[cfg(test)] pub mod tests { use super::*; const PARITY_CLIENT_SEMVER: &str = "2.4.0"; const PARITY_CLIENT_OLD_SEMVER: &str = "2.2.0"; const PARITY_CLIENT_OS: &str = "linux"; const PARITY_CLIENT_COMPILER: &str = "rustc"; const PARITY_CLIENT_IDENTITY: &str = "ExpanseSOLO"; const PARITY_CLIENT_MULTITOKEN_IDENTITY: &str = "ExpanseSOLO/abc/v1.2.3"; fn make_default_version_string() -> String { format!( "{}/v{}/{}/{}", CURRENT_CLIENT_ID_PREFIX, PARITY_CLIENT_SEMVER, PARITY_CLIENT_OS, PARITY_CLIENT_COMPILER ) } fn make_default_long_version_string() -> String { format!( "{}/{}/v{}/{}/{}", CURRENT_CLIENT_ID_PREFIX, PARITY_CLIENT_IDENTITY, PARITY_CLIENT_SEMVER, PARITY_CLIENT_OS, PARITY_CLIENT_COMPILER ) } fn make_multitoken_identity_long_version_string() -> String { format!( "{}/{}/v{}/{}/{}", CURRENT_CLIENT_ID_PREFIX, PARITY_CLIENT_MULTITOKEN_IDENTITY, PARITY_CLIENT_SEMVER, PARITY_CLIENT_OS, PARITY_CLIENT_COMPILER ) } fn make_old_semver_version_string() -> String { format!( "{}/v{}/{}/{}", CURRENT_CLIENT_ID_PREFIX, PARITY_CLIENT_OLD_SEMVER, PARITY_CLIENT_OS, PARITY_CLIENT_COMPILER ) } #[test] pub fn client_version_when_from_empty_string_then_default() { let default = ClientVersion::default(); assert_eq!(ClientVersion::from(""), default); } #[test] pub fn get_number_from_version_when_valid_then_number() { let version_string = format!("v{}", PARITY_CLIENT_SEMVER); assert_eq!( get_number_from_version(&version_string).unwrap(), PARITY_CLIENT_SEMVER ); } #[test] pub fn client_version_when_str_parity_format_and_valid_then_all_fields_match() { let client_version_string = make_default_version_string(); if let ClientVersion::ParityClient(client_version) = ClientVersion::from(client_version_string.as_str()) { assert_eq!(client_version.name(), CURRENT_CLIENT_ID_PREFIX); assert_eq!( *client_version.semver(), Version::parse(PARITY_CLIENT_SEMVER).unwrap() ); assert_eq!(client_version.os(), PARITY_CLIENT_OS); assert_eq!(client_version.compiler(), PARITY_CLIENT_COMPILER); } else { panic!("shouldn't be here"); } } #[test] pub fn client_version_when_str_parity_long_format_and_valid_then_all_fields_match() { let client_version_string = make_default_long_version_string(); if let ClientVersion::ParityClient(client_version) = ClientVersion::from(client_version_string.as_str()) { assert_eq!(client_version.name(), CURRENT_CLIENT_ID_PREFIX); assert_eq!(client_version.identity().unwrap(), PARITY_CLIENT_IDENTITY); assert_eq!( *client_version.semver(), Version::parse(PARITY_CLIENT_SEMVER).unwrap() ); assert_eq!(client_version.os(), PARITY_CLIENT_OS); assert_eq!(client_version.compiler(), PARITY_CLIENT_COMPILER); } else { panic!("shouldnt be here"); } } #[test] pub fn client_version_when_str_parity_long_format_and_valid_and_identity_multiple_tokens_then_all_fields_match( ) { let client_version_string = make_multitoken_identity_long_version_string(); if let ClientVersion::ParityClient(client_version) = ClientVersion::from(client_version_string.as_str()) { assert_eq!(client_version.name(), CURRENT_CLIENT_ID_PREFIX); assert_eq!( client_version.identity().unwrap(), PARITY_CLIENT_MULTITOKEN_IDENTITY ); assert_eq!( *client_version.semver(), Version::parse(PARITY_CLIENT_SEMVER).unwrap() ); assert_eq!(client_version.os(), PARITY_CLIENT_OS); assert_eq!(client_version.compiler(), PARITY_CLIENT_COMPILER); } else { panic!("shouldnt be here"); } } #[test] pub fn client_version_when_string_parity_format_and_valid_then_all_fields_match() { let client_version_string: String = make_default_version_string(); if let ClientVersion::ParityClient(client_version) = ClientVersion::from(client_version_string.as_str()) { assert_eq!(client_version.name(), CURRENT_CLIENT_ID_PREFIX); assert_eq!( *client_version.semver(), Version::parse(PARITY_CLIENT_SEMVER).unwrap() ); assert_eq!(client_version.os(), PARITY_CLIENT_OS); assert_eq!(client_version.compiler(), PARITY_CLIENT_COMPILER); } else { panic!("shouldn't be here"); } } #[test] pub fn client_version_when_parity_format_and_invalid_then_equals_parity_unknown_client_version_string( ) { // This is invalid because version has no leading 'v' let client_version_string = format!( "{}/{}/{}/{}", CURRENT_CLIENT_ID_PREFIX, PARITY_CLIENT_SEMVER, PARITY_CLIENT_OS, PARITY_CLIENT_COMPILER ); let client_version = ClientVersion::from(client_version_string.as_str()); let parity_unknown = ClientVersion::ParityUnknownFormat(client_version_string.to_string()); assert_eq!(client_version, parity_unknown); } #[test] pub fn client_version_when_parity_format_without_identity_and_missing_compiler_field_then_equals_parity_unknown_client_version_string( ) { let client_version_string = format!( "{}/v{}/{}", CURRENT_CLIENT_ID_PREFIX, PARITY_CLIENT_SEMVER, PARITY_CLIENT_OS, ); let client_version = ClientVersion::from(client_version_string.as_str()); let parity_unknown = ClientVersion::ParityUnknownFormat(client_version_string.to_string()); assert_eq!(client_version, parity_unknown); } #[test] pub fn client_version_when_parity_format_with_identity_and_missing_compiler_field_then_equals_parity_unknown_client_version_string( ) { let client_version_string = format!( "{}/{}/v{}/{}", CURRENT_CLIENT_ID_PREFIX, PARITY_CLIENT_IDENTITY, PARITY_CLIENT_SEMVER, PARITY_CLIENT_OS, ); let client_version = ClientVersion::from(client_version_string.as_str()); let parity_unknown = ClientVersion::ParityUnknownFormat(client_version_string.to_string()); assert_eq!(client_version, parity_unknown); } #[test] pub fn client_version_when_not_parity_format_and_valid_then_other_with_client_version_string() { let client_version_string = "Geth/main.jnode.network/v1.8.21-stable-9dc5d1a9/linux"; let client_version = ClientVersion::from(client_version_string); assert_eq!( client_version, ClientVersion::Other(client_version_string.to_string()) ); } #[test] pub fn client_version_when_parity_format_and_valid_then_to_string_equal() { let client_version_string: String = make_default_version_string(); let client_version = ClientVersion::from(client_version_string.as_str()); assert_eq!(client_version.to_string(), client_version_string); } #[test] pub fn client_version_when_other_then_to_string_equal_input_string() { let client_version_string: String = "Other".to_string(); let client_version = ClientVersion::from("Other"); assert_eq!(client_version.to_string(), client_version_string); } #[test] pub fn client_capabilities_when_parity_old_version_then_handles_large_requests_false() { let client_version_string: String = make_old_semver_version_string(); let client_version = ClientVersion::from(client_version_string.as_str()); assert!(!client_version.can_handle_large_requests()); } #[test] pub fn client_capabilities_when_parity_beta_version_then_not_handles_large_requests_true() { let client_version_string: String = format!( "{}/v{}/{}/{}", "Parity-Ethereum", "2.4.0-beta", "x86_64-linux-gnu", "rustc1.31.1" ) .to_string(); let client_version = ClientVersion::from(client_version_string.as_str()); assert!(!client_version.can_handle_large_requests()); } #[test] pub fn client_version_when_to_owned_then_both_objects_equal() { let client_version_string: String = make_old_semver_version_string(); let origin = ClientVersion::from(client_version_string.as_str()); let borrowed = &origin; let owned = origin.to_owned(); assert_eq!(*borrowed, owned); } #[test] fn client_version_accepts_service_transaction_for_different_versions() { assert!(!ClientVersion::from("Geth").accepts_service_transaction()); assert!( ClientVersion::from("Parity-Ethereum/v2.6.0/linux/rustc").accepts_service_transaction() ); assert!( ClientVersion::from("Parity-Ethereum/ABCDEFGH/v2.7.3/linux/rustc") .accepts_service_transaction() ); assert!( ClientVersion::from("OpenEthereum//v3.2.0/x86_64-linux-gnu/rustc1.49.0") .accepts_service_transaction() ); assert!(ClientVersion::from("OpenEthereum/ABCDEFGH").accepts_service_transaction()); assert!( ClientVersion::from("Nethermind/v1.10.37-0-068e5c399-20210311/X64-Linux/5.0.4") .accepts_service_transaction() ) } #[test] fn is_parity_when_parity_then_true() { let client_id = format!("{}/", CURRENT_CLIENT_ID_PREFIX); assert!(is_parity(&client_id)); } #[test] fn is_parity_when_empty_then_false() { let client_id = ""; assert!(!is_parity(&client_id)); } #[test] fn is_parity_when_other_then_false() { let client_id = "other"; assert!(!is_parity(&client_id)); } }