Skip to content

Commit

Permalink
feat: [torrust#1159] extract new package tracker api client
Browse files Browse the repository at this point in the history
  • Loading branch information
josecelano committed Jan 10, 2025
1 parent f6aca40 commit a1ded65
Show file tree
Hide file tree
Showing 12 changed files with 345 additions and 1 deletion.
1 change: 1 addition & 0 deletions .github/workflows/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ jobs:
cargo publish -p bittorrent-http-protocol
cargo publish -p bittorrent-tracker-client
cargo publish -p torrust-tracker
cargo publish -p torrust-tracker-api-client
cargo publish -p torrust-tracker-client
cargo publish -p torrust-tracker-clock
cargo publish -p torrust-tracker-configuration
Expand Down
10 changes: 10 additions & 0 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ rust-version.workspace = true
version.workspace = true

[lib]
name = "torrust_tracker_lib"
name = "torrust_tracker_lib"

[workspace.package]
authors = ["Nautilus Cyberneering <[email protected]>, Mick van Dijke <[email protected]>"]
Expand Down Expand Up @@ -108,6 +108,7 @@ members = [
"packages/primitives",
"packages/test-helpers",
"packages/torrent-repository",
"packages/tracker-api-client",
"packages/tracker-client",
]

Expand Down
21 changes: 21 additions & 0 deletions packages/tracker-api-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
description = "A library to interact with the Torrust Tracker REST API."
keywords = ["bittorrent", "client", "tracker"]
license = "LGPL-3.0"
name = "torrust-tracker-api-client"
readme = "README.md"

authors.workspace = true
documentation.workspace = true
edition.workspace = true
homepage.workspace = true
publish.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true

[dependencies]
hyper = "1"
reqwest = { version = "0", features = ["json"] }
serde = { version = "1", features = ["derive"] }
uuid = { version = "1", features = ["v4"] }
23 changes: 23 additions & 0 deletions packages/tracker-api-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Torrust Tracker API Client

A library to interact with the Torrust Tracker REST API.

## License

**Copyright (c) 2024 The Torrust Developers.**

This program is free software: you can redistribute it and/or modify it under the terms of the [GNU Lesser General Public License][LGPL_3_0] as published by the [Free Software Foundation][FSF], version 3.

This program 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 Lesser General Public License][LGPL_3_0] for more details.

You should have received a copy of the *GNU Lesser General Public License* along with this program. If not, see <https://www.gnu.org/licenses/>.

Some files include explicit copyright notices and/or license notices.

### Legacy Exception

For prosperity, versions of Torrust BitTorrent Tracker Client that are older than five years are automatically granted the [MIT-0][MIT_0] license in addition to the existing [LGPL-3.0-only][LGPL_3_0] license.

[LGPL_3_0]: ./LICENSE
[MIT_0]: ./docs/licenses/LICENSE-MIT_0
[FSF]: https://www.fsf.org/
14 changes: 14 additions & 0 deletions packages/tracker-api-client/docs/licenses/LICENSE-MIT_0
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
MIT No Attribution

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
57 changes: 57 additions & 0 deletions packages/tracker-api-client/src/common/http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
pub type ReqwestQuery = Vec<ReqwestQueryParam>;
pub type ReqwestQueryParam = (String, String);

/// URL Query component
#[derive(Default, Debug)]
pub struct Query {
params: Vec<QueryParam>,
}

impl Query {
#[must_use]
pub fn empty() -> Self {
Self { params: vec![] }
}

#[must_use]
pub fn params(params: Vec<QueryParam>) -> Self {
Self { params }
}

pub fn add_param(&mut self, param: QueryParam) {
self.params.push(param);
}
}

impl From<Query> for ReqwestQuery {
fn from(url_search_params: Query) -> Self {
url_search_params
.params
.iter()
.map(|param| ReqwestQueryParam::from((*param).clone()))
.collect()
}
}

/// URL query param
#[derive(Clone, Debug)]
pub struct QueryParam {
name: String,
value: String,
}

impl QueryParam {
#[must_use]
pub fn new(name: &str, value: &str) -> Self {
Self {
name: name.to_string(),
value: value.to_string(),
}
}
}

impl From<QueryParam> for ReqwestQueryParam {
fn from(param: QueryParam) -> Self {
(param.name, param.value)
}
}
1 change: 1 addition & 0 deletions packages/tracker-api-client/src/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod http;
33 changes: 33 additions & 0 deletions packages/tracker-api-client/src/connection_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#[must_use]
pub fn connection_with_invalid_token(bind_address: &str) -> ConnectionInfo {
ConnectionInfo::authenticated(bind_address, "invalid token")
}

#[must_use]
pub fn connection_with_no_token(bind_address: &str) -> ConnectionInfo {
ConnectionInfo::anonymous(bind_address)
}

#[derive(Clone)]
pub struct ConnectionInfo {
pub bind_address: String,
pub api_token: Option<String>,
}

impl ConnectionInfo {
#[must_use]
pub fn authenticated(bind_address: &str, api_token: &str) -> Self {
Self {
bind_address: bind_address.to_string(),
api_token: Some(api_token.to_string()),
}
}

#[must_use]
pub fn anonymous(bind_address: &str) -> Self {
Self {
bind_address: bind_address.to_string(),
api_token: None,
}
}
}
3 changes: 3 additions & 0 deletions packages/tracker-api-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod common;
pub mod connection_info;
pub mod v1;
179 changes: 179 additions & 0 deletions packages/tracker-api-client/src/v1/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
use hyper::HeaderMap;
use reqwest::Response;
use serde::Serialize;
use uuid::Uuid;

use crate::common::http::{Query, QueryParam, ReqwestQuery};
use crate::connection_info::ConnectionInfo;

/// API Client
pub struct Client {
connection_info: ConnectionInfo,
base_path: String,
}

impl Client {
#[must_use]
pub fn new(connection_info: ConnectionInfo) -> Self {
Self {
connection_info,
base_path: "/api/v1/".to_string(),
}
}

pub async fn generate_auth_key(&self, seconds_valid: i32, headers: Option<HeaderMap>) -> Response {
self.post_empty(&format!("key/{}", &seconds_valid), headers).await
}

pub async fn add_auth_key(&self, add_key_form: AddKeyForm, headers: Option<HeaderMap>) -> Response {
self.post_form("keys", &add_key_form, headers).await
}

pub async fn delete_auth_key(&self, key: &str, headers: Option<HeaderMap>) -> Response {
self.delete(&format!("key/{}", &key), headers).await
}

pub async fn reload_keys(&self, headers: Option<HeaderMap>) -> Response {
self.get("keys/reload", Query::default(), headers).await
}

pub async fn whitelist_a_torrent(&self, info_hash: &str, headers: Option<HeaderMap>) -> Response {
self.post_empty(&format!("whitelist/{}", &info_hash), headers).await
}

pub async fn remove_torrent_from_whitelist(&self, info_hash: &str, headers: Option<HeaderMap>) -> Response {
self.delete(&format!("whitelist/{}", &info_hash), headers).await
}

pub async fn reload_whitelist(&self, headers: Option<HeaderMap>) -> Response {
self.get("whitelist/reload", Query::default(), headers).await
}

pub async fn get_torrent(&self, info_hash: &str, headers: Option<HeaderMap>) -> Response {
self.get(&format!("torrent/{}", &info_hash), Query::default(), headers).await
}

pub async fn get_torrents(&self, params: Query, headers: Option<HeaderMap>) -> Response {
self.get("torrents", params, headers).await
}

pub async fn get_tracker_statistics(&self, headers: Option<HeaderMap>) -> Response {
self.get("stats", Query::default(), headers).await
}

pub async fn get(&self, path: &str, params: Query, headers: Option<HeaderMap>) -> Response {
let mut query: Query = params;

if let Some(token) = &self.connection_info.api_token {
query.add_param(QueryParam::new("token", token));
};

self.get_request_with_query(path, query, headers).await
}

/// # Panics
///
/// Will panic if the request can't be sent
pub async fn post_empty(&self, path: &str, headers: Option<HeaderMap>) -> Response {
let builder = reqwest::Client::new()
.post(self.base_url(path).clone())
.query(&ReqwestQuery::from(self.query_with_token()));

let builder = match headers {
Some(headers) => builder.headers(headers),
None => builder,
};

builder.send().await.unwrap()
}

/// # Panics
///
/// Will panic if the request can't be sent
pub async fn post_form<T: Serialize + ?Sized>(&self, path: &str, form: &T, headers: Option<HeaderMap>) -> Response {
let builder = reqwest::Client::new()
.post(self.base_url(path).clone())
.query(&ReqwestQuery::from(self.query_with_token()))
.json(&form);

let builder = match headers {
Some(headers) => builder.headers(headers),
None => builder,
};

builder.send().await.unwrap()
}

/// # Panics
///
/// Will panic if the request can't be sent
async fn delete(&self, path: &str, headers: Option<HeaderMap>) -> Response {
let builder = reqwest::Client::new()
.delete(self.base_url(path).clone())
.query(&ReqwestQuery::from(self.query_with_token()));

let builder = match headers {
Some(headers) => builder.headers(headers),
None => builder,
};

builder.send().await.unwrap()
}

pub async fn get_request_with_query(&self, path: &str, params: Query, headers: Option<HeaderMap>) -> Response {
get(&self.base_url(path), Some(params), headers).await
}

pub async fn get_request(&self, path: &str) -> Response {
get(&self.base_url(path), None, None).await
}

fn query_with_token(&self) -> Query {
match &self.connection_info.api_token {
Some(token) => Query::params([QueryParam::new("token", token)].to_vec()),
None => Query::default(),
}
}

fn base_url(&self, path: &str) -> String {
format!("http://{}{}{path}", &self.connection_info.bind_address, &self.base_path)
}
}

/// # Panics
///
/// Will panic if the request can't be sent
pub async fn get(path: &str, query: Option<Query>, headers: Option<HeaderMap>) -> Response {
let builder = reqwest::Client::builder().build().unwrap();

let builder = match query {
Some(params) => builder.get(path).query(&ReqwestQuery::from(params)),
None => builder.get(path),
};

let builder = match headers {
Some(headers) => builder.headers(headers),
None => builder,
};

builder.send().await.unwrap()
}

/// Returns a `HeaderMap` with a request id header
///
/// # Panics
///
/// Will panic if the request ID can't be parsed into a string.
#[must_use]
pub fn headers_with_request_id(request_id: Uuid) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("x-request-id", request_id.to_string().parse().unwrap());
headers
}

#[derive(Serialize, Debug)]
pub struct AddKeyForm {
#[serde(rename = "key")]
pub opt_key: Option<String>,
pub seconds_valid: Option<u64>,
}
1 change: 1 addition & 0 deletions packages/tracker-api-client/src/v1/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod client;

0 comments on commit a1ded65

Please sign in to comment.