Skip to content

Commit c5beff5

Browse files
committed
feat: [#979] permanent keys
This commit adds a new feature. It allow creating permanent keys (keys that do not expire). THis is an example for making a request to the endpoint using curl: ```console curl -X POST http://localhost:1212/api/v1/keys?token=MyAccessToken \ -H "Content-Type: application/json" \ -d '{ "key": null, "seconds_valid": null }' ``` NOTICE: both the `key` and the `seconds_valid` fields can be null. - If `key` is `null` a new random key will be generated. You can use an string with a pre-generated key like `Xc1L4PbQJSFGlrgSRZl8wxSFAuMa2110`. That will allow users to migrate to the Torrust Tracker wihtout forcing the users to re-start downloading/seeding with new keys. - If `seconds_valid` is `null` the key will be permanent. Otherwise it will expire after the seconds specified in this value.
1 parent 8d3fe72 commit c5beff5

20 files changed

+455
-167
lines changed

migrations/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Database Migrations
2+
3+
We don't support automatic migrations yet. The tracker creates all the needed tables when it starts. The SQL sentences are hardcoded in each database driver.
4+
5+
The migrations in this folder were introduced to add some new changes (permanent keys) and to allow users to migrate to the new version. In the future, we will remove the hardcoded SQL and start using a Rust crate for database migrations. For the time being, if you are using the initial schema described in the migration `20240730183000_torrust_tracker_create_all_tables.sql` you will need to run all the subsequent migrations manually.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
CREATE TABLE
2+
IF NOT EXISTS whitelist (
3+
id integer PRIMARY KEY AUTO_INCREMENT,
4+
info_hash VARCHAR(40) NOT NULL UNIQUE
5+
);
6+
7+
CREATE TABLE
8+
IF NOT EXISTS torrents (
9+
id integer PRIMARY KEY AUTO_INCREMENT,
10+
info_hash VARCHAR(40) NOT NULL UNIQUE,
11+
completed INTEGER DEFAULT 0 NOT NULL
12+
);
13+
14+
CREATE TABLE
15+
IF NOT EXISTS `keys` (
16+
`id` INT NOT NULL AUTO_INCREMENT,
17+
`key` VARCHAR(32) NOT NULL,
18+
`valid_until` INT (10) NOT NULL,
19+
PRIMARY KEY (`id`),
20+
UNIQUE (`key`)
21+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE `keys` CHANGE `valid_until` `valid_until` INT (10);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
CREATE TABLE
2+
IF NOT EXISTS whitelist (
3+
id INTEGER PRIMARY KEY AUTOINCREMENT,
4+
info_hash TEXT NOT NULL UNIQUE
5+
);
6+
7+
CREATE TABLE
8+
IF NOT EXISTS torrents (
9+
id INTEGER PRIMARY KEY AUTOINCREMENT,
10+
info_hash TEXT NOT NULL UNIQUE,
11+
completed INTEGER DEFAULT 0 NOT NULL
12+
);
13+
14+
CREATE TABLE
15+
IF NOT EXISTS keys (
16+
id INTEGER PRIMARY KEY AUTOINCREMENT,
17+
key TEXT NOT NULL UNIQUE,
18+
valid_until INTEGER NOT NULL
19+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CREATE TABLE
2+
IF NOT EXISTS keys_new (
3+
id INTEGER PRIMARY KEY AUTOINCREMENT,
4+
key TEXT NOT NULL UNIQUE,
5+
valid_until INTEGER
6+
);
7+
8+
INSERT INTO keys_new SELECT * FROM `keys`;
9+
10+
DROP TABLE `keys`;
11+
12+
ALTER TABLE keys_new RENAME TO `keys`;

src/core/auth.rs

+70-37
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//! Tracker keys are tokens used to authenticate the tracker clients when the tracker runs
55
//! in `private` or `private_listed` modes.
66
//!
7-
//! There are services to [`generate`] and [`verify`] authentication keys.
7+
//! There are services to [`generate_key`] and [`verify_key`] authentication keys.
88
//!
99
//! Authentication keys are used only by [`HTTP`](crate::servers::http) trackers. All keys have an expiration time, that means
1010
//! they are only valid during a period of time. After that time the expiring key will no longer be valid.
@@ -19,7 +19,7 @@
1919
//! /// Random 32-char string. For example: `YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ`
2020
//! pub key: Key,
2121
//! /// Timestamp, the key will be no longer valid after this timestamp
22-
//! pub valid_until: DurationSinceUnixEpoch,
22+
//! pub valid_until: Option<DurationSinceUnixEpoch>,
2323
//! }
2424
//! ```
2525
//!
@@ -29,11 +29,11 @@
2929
//! use torrust_tracker::core::auth;
3030
//! use std::time::Duration;
3131
//!
32-
//! let expiring_key = auth::generate(Duration::new(9999, 0));
32+
//! let expiring_key = auth::generate_key(Some(Duration::new(9999, 0)));
3333
//!
3434
//! // And you can later verify it with:
3535
//!
36-
//! assert!(auth::verify(&expiring_key).is_ok());
36+
//! assert!(auth::verify_key(&expiring_key).is_ok());
3737
//! ```
3838
3939
use std::panic::Location;
@@ -55,63 +55,96 @@ use tracing::debug;
5555
use crate::shared::bit_torrent::common::AUTH_KEY_LENGTH;
5656
use crate::CurrentClock;
5757

58+
/// It generates a new permanent random key [`PeerKey`].
5859
#[must_use]
59-
/// It generates a new random 32-char authentication [`ExpiringKey`]
60+
pub fn generate_permanent_key() -> PeerKey {
61+
generate_key(None)
62+
}
63+
64+
/// It generates a new random 32-char authentication [`PeerKey`].
65+
///
66+
/// It can be an expiring or permanent key.
6067
///
6168
/// # Panics
6269
///
6370
/// It would panic if the `lifetime: Duration` + Duration is more than `Duration::MAX`.
64-
pub fn generate(lifetime: Duration) -> ExpiringKey {
71+
///
72+
/// # Arguments
73+
///
74+
/// * `lifetime`: if `None` the key will be permanent.
75+
#[must_use]
76+
pub fn generate_key(lifetime: Option<Duration>) -> PeerKey {
6577
let random_id: String = thread_rng()
6678
.sample_iter(&Alphanumeric)
6779
.take(AUTH_KEY_LENGTH)
6880
.map(char::from)
6981
.collect();
7082

71-
debug!("Generated key: {}, valid for: {:?} seconds", random_id, lifetime);
83+
if let Some(lifetime) = lifetime {
84+
debug!("Generated key: {}, valid for: {:?} seconds", random_id, lifetime);
85+
86+
PeerKey {
87+
key: random_id.parse::<Key>().unwrap(),
88+
valid_until: Some(CurrentClock::now_add(&lifetime).unwrap()),
89+
}
90+
} else {
91+
debug!("Generated key: {}, permanent", random_id);
7292

73-
ExpiringKey {
74-
key: random_id.parse::<Key>().unwrap(),
75-
valid_until: CurrentClock::now_add(&lifetime).unwrap(),
93+
PeerKey {
94+
key: random_id.parse::<Key>().unwrap(),
95+
valid_until: None,
96+
}
7697
}
7798
}
7899

79-
/// It verifies an [`ExpiringKey`]. It checks if the expiration date has passed.
100+
/// It verifies an [`PeerKey`]. It checks if the expiration date has passed.
101+
/// Permanent keys without duration (`None`) do not expire.
80102
///
81103
/// # Errors
82104
///
83-
/// Will return `Error::KeyExpired` if `auth_key.valid_until` is past the `current_time`.
105+
/// Will return:
84106
///
85-
/// Will return `Error::KeyInvalid` if `auth_key.valid_until` is past the `None`.
86-
pub fn verify(auth_key: &ExpiringKey) -> Result<(), Error> {
107+
/// - `Error::KeyExpired` if `auth_key.valid_until` is past the `current_time`.
108+
/// - `Error::KeyInvalid` if `auth_key.valid_until` is past the `None`.
109+
pub fn verify_key(auth_key: &PeerKey) -> Result<(), Error> {
87110
let current_time: DurationSinceUnixEpoch = CurrentClock::now();
88111

89-
if auth_key.valid_until < current_time {
90-
Err(Error::KeyExpired {
91-
location: Location::caller(),
92-
})
93-
} else {
94-
Ok(())
112+
match auth_key.valid_until {
113+
Some(valid_until) => {
114+
if valid_until < current_time {
115+
Err(Error::KeyExpired {
116+
location: Location::caller(),
117+
})
118+
} else {
119+
Ok(())
120+
}
121+
}
122+
None => Ok(()), // Permanent key
95123
}
96124
}
97125

98-
/// An authentication key which has an expiration time.
126+
/// An authentication key which can potentially have an expiration time.
99127
/// After that time is will automatically become invalid.
100128
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
101-
pub struct ExpiringKey {
129+
pub struct PeerKey {
102130
/// Random 32-char string. For example: `YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ`
103131
pub key: Key,
104-
/// Timestamp, the key will be no longer valid after this timestamp
105-
pub valid_until: DurationSinceUnixEpoch,
132+
133+
/// Timestamp, the key will be no longer valid after this timestamp.
134+
/// If `None` the keys will not expire (permanent key).
135+
pub valid_until: Option<DurationSinceUnixEpoch>,
106136
}
107137

108-
impl std::fmt::Display for ExpiringKey {
138+
impl std::fmt::Display for PeerKey {
109139
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110-
write!(f, "key: `{}`, valid until `{}`", self.key, self.expiry_time())
140+
match self.expiry_time() {
141+
Some(expire_time) => write!(f, "key: `{}`, valid until `{}`", self.key, expire_time),
142+
None => write!(f, "key: `{}`, permanent", self.key),
143+
}
111144
}
112145
}
113146

114-
impl ExpiringKey {
147+
impl PeerKey {
115148
#[must_use]
116149
pub fn key(&self) -> Key {
117150
self.key.clone()
@@ -126,8 +159,8 @@ impl ExpiringKey {
126159
/// Will panic when the key timestamp overflows the internal i64 type.
127160
/// (this will naturally happen in 292.5 billion years)
128161
#[must_use]
129-
pub fn expiry_time(&self) -> chrono::DateTime<chrono::Utc> {
130-
convert_from_timestamp_to_datetime_utc(self.valid_until)
162+
pub fn expiry_time(&self) -> Option<chrono::DateTime<chrono::Utc>> {
163+
self.valid_until.map(convert_from_timestamp_to_datetime_utc)
131164
}
132165
}
133166

@@ -194,8 +227,8 @@ impl FromStr for Key {
194227
}
195228
}
196229

197-
/// Verification error. Error returned when an [`ExpiringKey`] cannot be
198-
/// verified with the [`verify(...)`](crate::core::auth::verify) function.
230+
/// Verification error. Error returned when an [`PeerKey`] cannot be
231+
/// verified with the (`crate::core::auth::verify_key`) function.
199232
#[derive(Debug, Error)]
200233
#[allow(dead_code)]
201234
pub enum Error {
@@ -277,7 +310,7 @@ mod tests {
277310
// Set the time to the current time.
278311
clock::Stopped::local_set_to_unix_epoch();
279312

280-
let expiring_key = auth::generate(Duration::from_secs(0));
313+
let expiring_key = auth::generate_key(Some(Duration::from_secs(0)));
281314

282315
assert_eq!(
283316
expiring_key.to_string(),
@@ -287,9 +320,9 @@ mod tests {
287320

288321
#[test]
289322
fn should_be_generated_with_a_expiration_time() {
290-
let expiring_key = auth::generate(Duration::new(9999, 0));
323+
let expiring_key = auth::generate_key(Some(Duration::new(9999, 0)));
291324

292-
assert!(auth::verify(&expiring_key).is_ok());
325+
assert!(auth::verify_key(&expiring_key).is_ok());
293326
}
294327

295328
#[test]
@@ -298,17 +331,17 @@ mod tests {
298331
clock::Stopped::local_set_to_system_time_now();
299332

300333
// Make key that is valid for 19 seconds.
301-
let expiring_key = auth::generate(Duration::from_secs(19));
334+
let expiring_key = auth::generate_key(Some(Duration::from_secs(19)));
302335

303336
// Mock the time has passed 10 sec.
304337
clock::Stopped::local_add(&Duration::from_secs(10)).unwrap();
305338

306-
assert!(auth::verify(&expiring_key).is_ok());
339+
assert!(auth::verify_key(&expiring_key).is_ok());
307340

308341
// Mock the time has passed another 10 sec.
309342
clock::Stopped::local_add(&Duration::from_secs(10)).unwrap();
310343

311-
assert!(auth::verify(&expiring_key).is_err());
344+
assert!(auth::verify_key(&expiring_key).is_err());
312345
}
313346
}
314347
}

src/core/databases/mod.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -195,19 +195,19 @@ pub trait Database: Sync + Send {
195195
/// # Errors
196196
///
197197
/// Will return `Err` if unable to load.
198-
fn load_keys(&self) -> Result<Vec<auth::ExpiringKey>, Error>;
198+
fn load_keys(&self) -> Result<Vec<auth::PeerKey>, Error>;
199199

200200
/// It gets an expiring authentication key from the database.
201201
///
202-
/// It returns `Some(ExpiringKey)` if a [`ExpiringKey`](crate::core::auth::ExpiringKey)
202+
/// It returns `Some(PeerKey)` if a [`PeerKey`](crate::core::auth::PeerKey)
203203
/// with the input [`Key`] exists, `None` otherwise.
204204
///
205205
/// # Context: Authentication Keys
206206
///
207207
/// # Errors
208208
///
209209
/// Will return `Err` if unable to load.
210-
fn get_key_from_keys(&self, key: &Key) -> Result<Option<auth::ExpiringKey>, Error>;
210+
fn get_key_from_keys(&self, key: &Key) -> Result<Option<auth::PeerKey>, Error>;
211211

212212
/// It adds an expiring authentication key to the database.
213213
///
@@ -216,7 +216,7 @@ pub trait Database: Sync + Send {
216216
/// # Errors
217217
///
218218
/// Will return `Err` if unable to save.
219-
fn add_key_to_keys(&self, auth_key: &auth::ExpiringKey) -> Result<usize, Error>;
219+
fn add_key_to_keys(&self, auth_key: &auth::PeerKey) -> Result<usize, Error>;
220220

221221
/// It removes an expiring authentication key from the database.
222222
///

src/core/databases/mysql.rs

+27-12
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ impl Database for Mysql {
6060
CREATE TABLE IF NOT EXISTS `keys` (
6161
`id` INT NOT NULL AUTO_INCREMENT,
6262
`key` VARCHAR({}) NOT NULL,
63-
`valid_until` INT(10) NOT NULL,
63+
`valid_until` INT(10),
6464
PRIMARY KEY (`id`),
6565
UNIQUE (`key`)
6666
);",
@@ -119,14 +119,20 @@ impl Database for Mysql {
119119
}
120120

121121
/// Refer to [`databases::Database::load_keys`](crate::core::databases::Database::load_keys).
122-
fn load_keys(&self) -> Result<Vec<auth::ExpiringKey>, Error> {
122+
fn load_keys(&self) -> Result<Vec<auth::PeerKey>, Error> {
123123
let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?;
124124

125125
let keys = conn.query_map(
126126
"SELECT `key`, valid_until FROM `keys`",
127-
|(key, valid_until): (String, i64)| auth::ExpiringKey {
128-
key: key.parse::<Key>().unwrap(),
129-
valid_until: Duration::from_secs(valid_until.unsigned_abs()),
127+
|(key, valid_until): (String, Option<i64>)| match valid_until {
128+
Some(valid_until) => auth::PeerKey {
129+
key: key.parse::<Key>().unwrap(),
130+
valid_until: Some(Duration::from_secs(valid_until.unsigned_abs())),
131+
},
132+
None => auth::PeerKey {
133+
key: key.parse::<Key>().unwrap(),
134+
valid_until: None,
135+
},
130136
},
131137
)?;
132138

@@ -197,28 +203,37 @@ impl Database for Mysql {
197203
}
198204

199205
/// Refer to [`databases::Database::get_key_from_keys`](crate::core::databases::Database::get_key_from_keys).
200-
fn get_key_from_keys(&self, key: &Key) -> Result<Option<auth::ExpiringKey>, Error> {
206+
fn get_key_from_keys(&self, key: &Key) -> Result<Option<auth::PeerKey>, Error> {
201207
let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?;
202208

203-
let query = conn.exec_first::<(String, i64), _, _>(
209+
let query = conn.exec_first::<(String, Option<i64>), _, _>(
204210
"SELECT `key`, valid_until FROM `keys` WHERE `key` = :key",
205211
params! { "key" => key.to_string() },
206212
);
207213

208214
let key = query?;
209215

210-
Ok(key.map(|(key, expiry)| auth::ExpiringKey {
211-
key: key.parse::<Key>().unwrap(),
212-
valid_until: Duration::from_secs(expiry.unsigned_abs()),
216+
Ok(key.map(|(key, opt_valid_until)| match opt_valid_until {
217+
Some(valid_until) => auth::PeerKey {
218+
key: key.parse::<Key>().unwrap(),
219+
valid_until: Some(Duration::from_secs(valid_until.unsigned_abs())),
220+
},
221+
None => auth::PeerKey {
222+
key: key.parse::<Key>().unwrap(),
223+
valid_until: None,
224+
},
213225
}))
214226
}
215227

216228
/// Refer to [`databases::Database::add_key_to_keys`](crate::core::databases::Database::add_key_to_keys).
217-
fn add_key_to_keys(&self, auth_key: &auth::ExpiringKey) -> Result<usize, Error> {
229+
fn add_key_to_keys(&self, auth_key: &auth::PeerKey) -> Result<usize, Error> {
218230
let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?;
219231

220232
let key = auth_key.key.to_string();
221-
let valid_until = auth_key.valid_until.as_secs().to_string();
233+
let valid_until = match auth_key.valid_until {
234+
Some(valid_until) => valid_until.as_secs().to_string(),
235+
None => todo!(),
236+
};
222237

223238
conn.exec_drop(
224239
"INSERT INTO `keys` (`key`, valid_until) VALUES (:key, :valid_until)",

0 commit comments

Comments
 (0)