2026-06-20 20:43:23 +08:00
|
|
|
|
//! 用户存储与登录校验 + 开发者入口门。
|
2026-06-18 00:00:38 +08:00
|
|
|
|
//!
|
2026-06-20 20:43:23 +08:00
|
|
|
|
//! 两者默认都走 **MySQL**,且**每次校验直接查库(走 IO,不在内存里缓存整表)**——
|
|
|
|
|
|
//! 机器内存紧张,宁可每次打一次很轻的索引查询,也不常驻一份副本:
|
|
|
|
|
|
//! - 登录:表 `ccuser(username, password_sha256)`;
|
|
|
|
|
|
//! - 入口门:表 `dev_gate(gate_hash)`。
|
|
|
|
|
|
//! 连接串由环境变量 `DATABASE_URL` 指定,`UserStore` 与 `DevGate` **共用同一个
|
|
|
|
|
|
//! 连接池**(见 `db_pool_from_env`),避免开两份连接。未设置 `DATABASE_URL` 时各自
|
|
|
|
|
|
//! 回退到内置/文件兜底,方便本地无 DB 时开发。
|
2026-06-18 00:00:38 +08:00
|
|
|
|
//!
|
2026-06-20 20:43:23 +08:00
|
|
|
|
//! 口令永不明文存储:库里存的是 SHA256(hex),登录时把传入明文同样
|
2026-06-18 00:00:38 +08:00
|
|
|
|
//! 哈希后做**等长比较**。(注意:SHA256 仅用于“不落明文”,并非抗暴力破解的
|
|
|
|
|
|
//! 口令哈希;若将来要对外网开放,应换 Argon2/bcrypt 这类慢哈希。)
|
|
|
|
|
|
|
2026-06-20 20:43:23 +08:00
|
|
|
|
use std::collections::{HashMap, HashSet};
|
2026-06-18 00:00:38 +08:00
|
|
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
|
|
|
2026-06-24 20:57:37 +08:00
|
|
|
|
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
2026-06-18 00:00:38 +08:00
|
|
|
|
use sha2::{Digest, Sha256};
|
2026-06-20 20:43:23 +08:00
|
|
|
|
use sqlx::mysql::{MySqlPool, MySqlPoolOptions};
|
2026-06-18 00:00:38 +08:00
|
|
|
|
|
2026-06-20 20:43:23 +08:00
|
|
|
|
/// 按 `DATABASE_URL` 建一个**懒连接**池(启动不阻塞,首次查询才真正建连),
|
|
|
|
|
|
/// 供 `UserStore` 与 `DevGate` 共用。未设置该环境变量则返回 `None`,调用方各自兜底。
|
|
|
|
|
|
pub fn db_pool_from_env() -> Option<MySqlPool> {
|
|
|
|
|
|
match std::env::var("DATABASE_URL") {
|
|
|
|
|
|
Ok(url) if !url.trim().is_empty() => {
|
|
|
|
|
|
let pool = MySqlPoolOptions::new()
|
|
|
|
|
|
.max_connections(5)
|
|
|
|
|
|
.connect_lazy(&url)
|
|
|
|
|
|
.expect("DATABASE_URL 格式非法");
|
|
|
|
|
|
Some(pool)
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 开发者「入口门」:一组合法的入口 hash。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 访问 `域名/#<hash>` 时,前端把 `<hash>` 发给后端,只有命中这里的某一个,
|
|
|
|
|
|
/// 才放行去显示登录页。这样合法 hash **只存在后端**,不进前端打包产物,
|
|
|
|
|
|
/// 扒前端 JS 也看不到;且每个开发者一串、可单独增删。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 注意:入口 hash 只是“藏门”的共享口令,**不是身份凭证**。真正证明身份仍靠
|
|
|
|
|
|
/// 登录(用户名 + 密码,见 `UserStore`)。
|
|
|
|
|
|
pub enum DevGate {
|
|
|
|
|
|
/// 每次校验查 MySQL `dev_gate` 表(不缓存整表)。
|
|
|
|
|
|
Db(MySqlPool),
|
|
|
|
|
|
/// 内置/文件载入的 hash 集合,仅作无 DB 时的兜底。
|
|
|
|
|
|
Memory(HashSet<String>),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl DevGate {
|
|
|
|
|
|
/// 有连接池就走 MySQL;否则读 `GATE_FILE`(默认 `dev_gate.txt`),文件也没有再 `seed()`。
|
|
|
|
|
|
pub fn new(pool: Option<MySqlPool>) -> Self {
|
|
|
|
|
|
match pool {
|
|
|
|
|
|
Some(p) => {
|
|
|
|
|
|
eprintln!("[gate] 入口校验走 MySQL");
|
|
|
|
|
|
DevGate::Db(p)
|
|
|
|
|
|
}
|
|
|
|
|
|
None => Self::load_file(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 从 `GATE_FILE`(默认 `dev_gate.txt`)读入口 hash;读不到回退内置 `seed()`。
|
|
|
|
|
|
/// 文件格式:一行一个 hash,`#` 之后为注释,空行忽略。
|
|
|
|
|
|
fn load_file() -> Self {
|
|
|
|
|
|
let path = std::env::var("GATE_FILE").unwrap_or_else(|_| "dev_gate.txt".to_string());
|
|
|
|
|
|
match std::fs::read_to_string(&path) {
|
|
|
|
|
|
Ok(text) => {
|
|
|
|
|
|
let hashes = Self::parse(&text);
|
|
|
|
|
|
eprintln!("[gate] 从 {path} 载入 {} 个入口 hash", hashes.len());
|
|
|
|
|
|
DevGate::Memory(hashes)
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(_) => {
|
|
|
|
|
|
eprintln!("[gate] 未找到 {path},回退到内置写死列表");
|
|
|
|
|
|
Self::seed()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 把文件文本解析为 hash 集合:丢掉 `#` 注释与空行,每行取剩下的非空串。
|
|
|
|
|
|
fn parse(text: &str) -> HashSet<String> {
|
|
|
|
|
|
text.lines()
|
|
|
|
|
|
.map(|line| line.split('#').next().unwrap_or("").trim())
|
|
|
|
|
|
.filter(|s| !s.is_empty())
|
|
|
|
|
|
.map(|s| s.to_string())
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 内置写死的入口 hash 表,作为无 DB / 无文件时的兜底。
|
|
|
|
|
|
pub fn seed() -> Self {
|
|
|
|
|
|
let mut hashes = HashSet::new();
|
|
|
|
|
|
|
|
|
|
|
|
// cc:SHA256("192118Lht") 的前 12 位。对应入口 域名/#0086e83c9b10
|
|
|
|
|
|
hashes.insert("0086e83c9b10".to_string());
|
|
|
|
|
|
|
|
|
|
|
|
DevGate::Memory(hashes)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 该 hash 是否属于某个合法开发者。DB 模式下每次查库(命中即放行);
|
|
|
|
|
|
/// 查询出错按“不放行”处理,不泄露后端内部状态。
|
|
|
|
|
|
pub async fn allows(&self, hash: &str) -> bool {
|
|
|
|
|
|
match self {
|
|
|
|
|
|
DevGate::Memory(hashes) => hashes.contains(hash),
|
|
|
|
|
|
DevGate::Db(pool) => {
|
|
|
|
|
|
let row: Result<Option<(String,)>, sqlx::Error> =
|
|
|
|
|
|
sqlx::query_as("SELECT gate_hash FROM dev_gate WHERE gate_hash = ?")
|
|
|
|
|
|
.bind(hash)
|
|
|
|
|
|
.fetch_optional(pool)
|
|
|
|
|
|
.await;
|
|
|
|
|
|
match row {
|
|
|
|
|
|
Ok(found) => found.is_some(),
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
eprintln!("[gate] 查询 dev_gate 失败: {e}");
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 用户校验后端:优先连 MySQL,无 `DATABASE_URL` 时退回内置写死表。
|
|
|
|
|
|
pub enum UserStore {
|
|
|
|
|
|
/// 从 MySQL 的 `ccuser` 表查 `password_sha256`。
|
|
|
|
|
|
Db(MySqlPool),
|
|
|
|
|
|
/// 内置写死表(用户名 → 密码SHA256 hex),仅作无 DB 时的兜底。
|
|
|
|
|
|
Memory(HashMap<String, String>),
|
2026-06-18 00:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl UserStore {
|
2026-06-20 20:43:23 +08:00
|
|
|
|
/// 有连接池就走 MySQL(每次校验查 `ccuser`);否则回退到内置 `seed()`。
|
|
|
|
|
|
pub fn new(pool: Option<MySqlPool>) -> Self {
|
|
|
|
|
|
match pool {
|
|
|
|
|
|
Some(p) => {
|
|
|
|
|
|
eprintln!("[auth] 用户校验走 MySQL");
|
|
|
|
|
|
UserStore::Db(p)
|
|
|
|
|
|
}
|
|
|
|
|
|
None => {
|
|
|
|
|
|
eprintln!("[auth] 未设置 DATABASE_URL,回退到内置写死用户表");
|
|
|
|
|
|
Self::seed()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 内置写死的单用户表,作为 `load()` 无 DB 配置时的兜底。
|
2026-06-18 00:00:38 +08:00
|
|
|
|
pub fn seed() -> Self {
|
|
|
|
|
|
let mut users = HashMap::new();
|
|
|
|
|
|
|
|
|
|
|
|
// cc / 192118Lht
|
|
|
|
|
|
// 下面这串是 SHA256("192118Lht") 的 hex,明文不进源码。
|
|
|
|
|
|
users.insert(
|
|
|
|
|
|
"cc".to_string(),
|
|
|
|
|
|
"0086e83c9b108b227eed55425e9641286f42bd0c31a1b95afbb9edd6d3aa6234".to_string(),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-06-20 20:43:23 +08:00
|
|
|
|
UserStore::Memory(users)
|
2026-06-18 00:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 校验用户名 + 明文密码是否匹配。
|
|
|
|
|
|
///
|
2026-06-20 20:43:23 +08:00
|
|
|
|
/// 用户名不存在、密码错误、乃至 DB 查询出错,都一律返回 `false`,
|
|
|
|
|
|
/// 不向外区分(避免泄露“某用户名是否存在”或后端内部状态)。
|
|
|
|
|
|
pub async fn verify(&self, username: &str, password: &str) -> bool {
|
|
|
|
|
|
let actual_hex = sha256_hex(password);
|
|
|
|
|
|
let expected_hex = match self {
|
|
|
|
|
|
UserStore::Memory(users) => users.get(username).cloned(),
|
|
|
|
|
|
UserStore::Db(pool) => {
|
|
|
|
|
|
let row: Result<Option<(String,)>, sqlx::Error> =
|
|
|
|
|
|
sqlx::query_as("SELECT password_sha256 FROM ccuser WHERE username = ?")
|
|
|
|
|
|
.bind(username)
|
|
|
|
|
|
.fetch_optional(pool)
|
|
|
|
|
|
.await;
|
|
|
|
|
|
match row {
|
|
|
|
|
|
Ok(found) => found.map(|(hex,)| hex),
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
eprintln!("[auth] 查询 ccuser 失败: {e}");
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-18 00:00:38 +08:00
|
|
|
|
}
|
2026-06-20 20:43:23 +08:00
|
|
|
|
};
|
|
|
|
|
|
match expected_hex {
|
|
|
|
|
|
Some(expected) => constant_time_eq(actual_hex.as_bytes(), expected.as_bytes()),
|
2026-06-18 00:00:38 +08:00
|
|
|
|
None => false,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 22:42:06 +08:00
|
|
|
|
/// 照片墙用户校验后端:查 `wall_users` 表(与 `ccuser` 物理隔离)。
|
|
|
|
|
|
/// 未设 DATABASE_URL 时退回内置兜底。
|
|
|
|
|
|
pub enum WallUserStore {
|
|
|
|
|
|
Db(MySqlPool),
|
|
|
|
|
|
Memory(HashMap<String, String>),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl WallUserStore {
|
|
|
|
|
|
pub fn new(pool: Option<MySqlPool>) -> Self {
|
|
|
|
|
|
match pool {
|
|
|
|
|
|
Some(p) => {
|
|
|
|
|
|
eprintln!("[auth] 照片墙用户校验走 MySQL (wall_users)");
|
|
|
|
|
|
WallUserStore::Db(p)
|
|
|
|
|
|
}
|
|
|
|
|
|
None => {
|
|
|
|
|
|
eprintln!("[auth] 未设置 DATABASE_URL,照片墙用户回退到内置表");
|
|
|
|
|
|
Self::seed()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 内置兜底:cc + UserTest
|
|
|
|
|
|
pub fn seed() -> Self {
|
|
|
|
|
|
let mut users = HashMap::new();
|
|
|
|
|
|
// cc / 192118Lht
|
|
|
|
|
|
users.insert(
|
|
|
|
|
|
"cc".to_string(),
|
|
|
|
|
|
"0086e83c9b108b227eed55425e9641286f42bd0c31a1b95afbb9edd6d3aa6234".to_string(),
|
|
|
|
|
|
);
|
|
|
|
|
|
// UserTest / 2015.08.607
|
|
|
|
|
|
users.insert(
|
|
|
|
|
|
"UserTest".to_string(),
|
|
|
|
|
|
"76f03f2eaa05e66c82d25e168fec27e7d0d202f15da73aa5852562f863633b8e".to_string(),
|
|
|
|
|
|
);
|
2026-06-25 19:42:06 +08:00
|
|
|
|
// UserTest6340 / 2019.01.6340
|
|
|
|
|
|
users.insert(
|
|
|
|
|
|
"UserTest6340".to_string(),
|
|
|
|
|
|
"c7780d075f561283ae4665a64d8b8e2642389ad948390a542b847833a7ce7c1a".to_string(),
|
|
|
|
|
|
);
|
2026-06-24 22:42:06 +08:00
|
|
|
|
WallUserStore::Memory(users)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub async fn verify(&self, username: &str, password: &str) -> bool {
|
|
|
|
|
|
let actual_hex = sha256_hex(password);
|
|
|
|
|
|
let expected_hex = match self {
|
|
|
|
|
|
WallUserStore::Memory(users) => users.get(username).cloned(),
|
|
|
|
|
|
WallUserStore::Db(pool) => {
|
|
|
|
|
|
let row: Result<Option<(String,)>, sqlx::Error> =
|
|
|
|
|
|
sqlx::query_as("SELECT password_sha256 FROM wall_users WHERE username = ?")
|
|
|
|
|
|
.bind(username)
|
|
|
|
|
|
.fetch_optional(pool)
|
|
|
|
|
|
.await;
|
|
|
|
|
|
match row {
|
|
|
|
|
|
Ok(found) => found.map(|(hex,)| hex),
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
eprintln!("[auth] 查询 wall_users 失败: {e}");
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
match expected_hex {
|
|
|
|
|
|
Some(expected) => constant_time_eq(actual_hex.as_bytes(), expected.as_bytes()),
|
|
|
|
|
|
None => false,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 20:57:37 +08:00
|
|
|
|
/// JWT 载荷:`sub` 为用户名,`exp` 为到期 Unix 秒。
|
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
|
|
struct Claims {
|
|
|
|
|
|
sub: String,
|
|
|
|
|
|
exp: usize,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 签发登录 token:带用户名的 HS256 JWT,30 天过期。密钥取 `JWT_SECRET`。
|
|
|
|
|
|
pub fn issue_token(username: &str) -> String {
|
|
|
|
|
|
let exp = now_secs() + 30 * 24 * 3600;
|
|
|
|
|
|
sign(&jwt_secret(), username, exp)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 校验 token,成功返回其中的用户名。过期/签名不符/格式错均返回 `None`。
|
|
|
|
|
|
pub fn verify_token(token: &str) -> Option<String> {
|
|
|
|
|
|
verify(&jwt_secret(), token)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 用指定密钥签一个 JWT(便于测试,不读环境变量)。
|
|
|
|
|
|
fn sign(secret: &[u8], username: &str, exp: usize) -> String {
|
|
|
|
|
|
let claims = Claims {
|
|
|
|
|
|
sub: username.to_string(),
|
|
|
|
|
|
exp,
|
|
|
|
|
|
};
|
|
|
|
|
|
encode(
|
|
|
|
|
|
&Header::default(),
|
|
|
|
|
|
&claims,
|
|
|
|
|
|
&EncodingKey::from_secret(secret),
|
|
|
|
|
|
)
|
2026-06-24 21:00:21 +08:00
|
|
|
|
.expect("JWT 编码失败")
|
2026-06-24 20:57:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 用指定密钥校验 JWT(便于测试)。默认校验 `exp`。
|
|
|
|
|
|
fn verify(secret: &[u8], token: &str) -> Option<String> {
|
|
|
|
|
|
decode::<Claims>(
|
|
|
|
|
|
token,
|
|
|
|
|
|
&DecodingKey::from_secret(secret),
|
|
|
|
|
|
&Validation::default(),
|
|
|
|
|
|
)
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|data| data.claims.sub)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 从 `JWT_SECRET` 取签名密钥;未设置则用内置开发密钥并告警(生产务必设置)。
|
|
|
|
|
|
fn jwt_secret() -> Vec<u8> {
|
|
|
|
|
|
match std::env::var("JWT_SECRET") {
|
|
|
|
|
|
Ok(s) if !s.trim().is_empty() => s.into_bytes(),
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
eprintln!("[auth] 警告:未设置 JWT_SECRET,使用内置开发密钥(生产环境务必设置)");
|
|
|
|
|
|
b"dev-insecure-secret-change-me".to_vec()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn now_secs() -> usize {
|
|
|
|
|
|
SystemTime::now()
|
2026-06-18 00:00:38 +08:00
|
|
|
|
.duration_since(UNIX_EPOCH)
|
2026-06-24 20:57:37 +08:00
|
|
|
|
.map(|d| d.as_secs())
|
|
|
|
|
|
.unwrap_or(0) as usize
|
2026-06-18 00:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 计算 SHA256 并返回小写 hex 字符串。
|
|
|
|
|
|
fn sha256_hex(input: &str) -> String {
|
|
|
|
|
|
let digest = Sha256::digest(input.as_bytes());
|
|
|
|
|
|
let mut out = String::with_capacity(digest.len() * 2);
|
|
|
|
|
|
for byte in digest {
|
|
|
|
|
|
out.push_str(&format!("{byte:02x}"));
|
|
|
|
|
|
}
|
|
|
|
|
|
out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 等长定值时间比较,避免按字节短路造成的时序侧信道。
|
|
|
|
|
|
/// 两串长度不同直接判否(hex 定长 64,正常等长)。
|
|
|
|
|
|
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
|
|
|
|
|
|
if a.len() != b.len() {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
let mut diff = 0u8;
|
|
|
|
|
|
for (x, y) in a.iter().zip(b.iter()) {
|
|
|
|
|
|
diff |= x ^ y;
|
|
|
|
|
|
}
|
|
|
|
|
|
diff == 0
|
|
|
|
|
|
}
|
2026-06-24 20:57:37 +08:00
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod tests {
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn jwt_round_trips_username() {
|
|
|
|
|
|
let secret = b"test-secret";
|
|
|
|
|
|
let token = sign(secret, "cc", far_future());
|
|
|
|
|
|
assert_eq!(verify(secret, &token), Some("cc".to_string()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn jwt_rejects_wrong_secret() {
|
|
|
|
|
|
let token = sign(b"secret-a", "cc", far_future());
|
|
|
|
|
|
assert_eq!(verify(b"secret-b", &token), None);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn jwt_rejects_garbage() {
|
|
|
|
|
|
assert_eq!(verify(b"s", "not-a-jwt"), None);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn jwt_rejects_expired() {
|
|
|
|
|
|
let token = sign(b"s", "cc", 1); // 1970 年过期
|
|
|
|
|
|
assert_eq!(verify(b"s", &token), None);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn far_future() -> usize {
|
|
|
|
|
|
(std::time::SystemTime::now()
|
|
|
|
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
.as_secs()
|
|
|
|
|
|
+ 3600) as usize
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|