Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom headers #47

Merged
merged 12 commits into from
Feb 8, 2022
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Dropped generic authentication and added support for custom headers. <https://github.com/near/near-jsonrpc-client-rs/pull/47>
- Executing the [examples](https://github.com/near/near-jsonrpc-client-rs/tree/master/examples) now allows custom RPC addr specification with interactive server selection. <https://github.com/near/near-jsonrpc-client-rs/commit/b130118d0de806bd9950be306f563559f07c77e6> <https://github.com/near/near-jsonrpc-client-rs/commit/c5e938a90703cb216e99d6f23a43ad9d3812df3d>
- `JsonRpcClient::connect` is now generic over any string-like type. `&str`, `String` and `&String` are all supported. <https://github.com/near/near-jsonrpc-client-rs/pull/35>
- `JsonRpcClient::connect` is now generic over any url-like type. [`Url`](https://docs.rs/url/*/url/struct.Url.html), `&str`, `String` and `&String` are all supported. <https://github.com/near/near-jsonrpc-client-rs/pull/35>
- `JsonRpcClient` now defaults to the `Unauthenticated` state, easing a type specification pain point. <https://github.com/near/near-jsonrpc-client-rs/pull/36>

## [0.2.0] - 2021-12-22
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ thiserror = "1.0.28"
reqwest = { version = "0.11.4", features = ["json"] }
lazy_static = "1.4.0"

uuid = { version = "0.8", features = ["v4"] }
borsh = "0.9"
serde = "1.0.127"
serde_json = "1.0.66"
Expand Down
8 changes: 6 additions & 2 deletions examples/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ use near_primitives::types::{BlockReference, Finality};
async fn unauthorized() -> Result<(), Box<dyn std::error::Error>> {
let client = JsonRpcClient::connect("https://testnet.rpc.near.dev/");

let response = client.call(methods::status::RpcStatusRequest).await;
let request = methods::block::RpcBlockRequest {
block_reference: BlockReference::Finality(Finality::Final),
};

let response = client.call(request).await;

debug_assert!(
matches!(
Expand All @@ -24,7 +28,7 @@ async fn unauthorized() -> Result<(), Box<dyn std::error::Error>> {

async fn authorized() -> Result<(), Box<dyn std::error::Error>> {
let client = JsonRpcClient::connect("https://testnet.rpc.near.dev/")
.auth(auth::ApiKey::new("399ba741-e939-4ffa-8c3c-306ec36fa8de"));
.header(auth::ApiKey::new("399ba741-e939-4ffa-8c3c-306ec36fa8de")?);

let request = methods::block::RpcBlockRequest {
block_reference: BlockReference::Finality(Finality::Final),
Expand Down
2 changes: 1 addition & 1 deletion examples/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fn select<S>(print_msg: fn(), query: &str, chk: fn(&str) -> Option<S>) -> io::Re
}
}

pub fn select_network() -> io::Result<JsonRpcClient<auth::Unauthenticated>> {
pub fn select_network() -> io::Result<JsonRpcClient> {
println!("========[Network Selection]========");
let network = select(
|| {
Expand Down
139 changes: 101 additions & 38 deletions src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,59 +1,122 @@
pub struct AuthHeaderEntry<'a> {
pub header: &'a str,
pub value: &'a str,
}
use std::fmt;

mod private {
pub trait AuthState {
fn maybe_auth_header(&self) -> Option<super::AuthHeaderEntry>;
use reqwest::header::HeaderValue;

/// NEAR JSON RPC API key.
#[derive(Eq, Hash, Clone, Debug, PartialEq)]
pub struct ApiKey(HeaderValue);

impl ApiKey {
pub const HEADER_NAME: &'static str = "x-api-key";

/// Creates a new API key from a string.
pub fn new<K: IntoApiKey>(api_key: K) -> Result<Self, InvalidApiKey> {
if let Ok(api_key) = uuid::Uuid::parse_str(api_key.as_ref()) {
if let Ok(api_key) = api_key.to_string().try_into() {
return Ok(ApiKey(api_key));
}
}
Err(InvalidApiKey { _priv: () })
}

/// Returns the API key as a string slice.
pub fn as_str(&self) -> &str {
self.0
.to_str()
.expect("fatal: api key should contain only ascii characters")
}
}

pub trait AuthState: private::AuthState {}
impl crate::header::HeaderEntry for ApiKey {
type HeaderName = &'static str;
type HeaderValue = HeaderValue;

fn header_name(&self) -> &Self::HeaderName {
&Self::HEADER_NAME
}

#[derive(Debug, Clone)]
pub struct Unauthenticated;
impl AuthState for Unauthenticated {}
impl private::AuthState for Unauthenticated {
fn maybe_auth_header(&self) -> Option<AuthHeaderEntry> {
None
fn header_pair(self) -> (Self::HeaderName, Self::HeaderValue) {
(Self::HEADER_NAME, self.0)
}
}

pub trait AuthScheme {
fn get_auth_header(&self) -> AuthHeaderEntry;
impl fmt::Display for ApiKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "x-api-key: {}", self.as_str())
}
}

#[derive(Debug, Clone)]
pub struct Authenticated<T> {
pub(crate) auth_scheme: T,
/// An error returned when an API key contains invalid characters.
#[derive(Eq, Clone, PartialEq)]
pub struct InvalidApiKey {
_priv: (),
}

impl<T: AuthScheme> AuthState for Authenticated<T> {}
impl<T: AuthScheme> private::AuthState for Authenticated<T> {
fn maybe_auth_header(&self) -> Option<AuthHeaderEntry> {
Some(self.auth_scheme.get_auth_header())
impl fmt::Debug for InvalidApiKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("InvalidApiKey")
}
}

#[derive(Clone, Debug)]
pub struct ApiKey(String);

impl ApiKey {
pub fn new(api_key: impl Into<String>) -> Self {
Self(api_key.into())
impl std::error::Error for InvalidApiKey {}
impl fmt::Display for InvalidApiKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Invalid API key")
}
}

pub fn as_str(&self) -> &str {
&self.0
}
mod private {
pub trait Sealed: AsRef<str> {}
}

impl AuthScheme for ApiKey {
fn get_auth_header(&self) -> AuthHeaderEntry {
AuthHeaderEntry {
header: "x-api-key",
value: self.0.as_str(),
}
/// A marker trait used to identify values that can be made into API keys.
pub trait IntoApiKey: private::Sealed {}

impl private::Sealed for String {}

impl IntoApiKey for String {}

impl private::Sealed for &String {}

impl IntoApiKey for &String {}

impl private::Sealed for &str {}

impl IntoApiKey for &str {}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn api_key() {
ApiKey::new("some-value").expect_err("should not have been a valid API key");

ApiKey::new("0ee1872b-355f-4254-8e2b-1c0b8199ee92")
.expect("should have been a valid API key");

ApiKey::new("0ee1872b355f42548e2b1c0b8199ee92").expect("should have been a valid API key");

ApiKey::new("0ee--1872b355f4254-8e2b1c0b8-199ee92")
.expect_err("should not have been a valid API key");
}

#[test]
fn display() {
let api_key = ApiKey::new("0ee1872b-355f-4254-8e2b-1c0b8199ee92")
.expect("should have been a valid API key");

assert_eq!(
api_key.to_string(),
"x-api-key: 0ee1872b-355f-4254-8e2b-1c0b8199ee92"
);

let api_key = ApiKey::new("0ee1872b355f42548e2b1c0b8199ee92")
.expect("should have been a valid API key");

assert_eq!(
api_key.to_string(),
"x-api-key: 0ee1872b-355f-4254-8e2b-1c0b8199ee92"
);
}
}
Loading