Skip to content

Commit f19c297

Browse files
committed
add cert query API for tests
1 parent 733d7d4 commit f19c297

12 files changed

+130
-34
lines changed

Cargo.lock

+3-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "spa-client"
3-
version = "2.2.3"
3+
version = "2.2.4"
44
edition = "2021"
55
authors = ["timzaak"]
66
license = "MIT"

client/src/api.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use spa_server::admin_server::request::{
55
DeleteDomainVersionOption, DomainWithOptVersionOption, GetDomainOption,
66
UpdateUploadingStatusOption,
77
};
8-
use spa_server::domain_storage::{DomainInfo, ShortMetaData, UploadDomainPosition};
8+
use spa_server::domain_storage::{CertInfo, DomainInfo, ShortMetaData, UploadDomainPosition};
99
use std::borrow::Cow;
1010
use std::path::PathBuf;
1111

@@ -185,6 +185,12 @@ impl API {
185185
.await?;
186186
json_resp!(resp, UploadDomainPosition)
187187
}
188+
189+
pub async fn get_acme_cert_info(&self, domain: Option<String>) -> anyhow::Result<Vec<CertInfo>> {
190+
let resp = self.async_client.get(self.url("cert/acme"))
191+
.query(&GetDomainOption{domain}).send().await?;
192+
json_resp!(resp, Vec<CertInfo>)
193+
}
188194
}
189195
#[cfg(test)]
190196
mod test {

config.release.conf

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ file_dir = "/data"
2828
// # directory to store account and certificate
2929
// # optional, default is ${file_dir}/acme
3030
// // dir = "/data/acme"
31-
// # ci / stage / prod, default is prod
31+
// # ci / stage / prod, default is prod, ci is just for CI test with Pebble, don't use it.
3232
// //type = prod
3333
// }
3434

docs/develop/change-log.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# Change Log
2+
3+
### Version 2.2.4
4+
- feat: add cert query API (no doc, no client SDK support)
25
### Version 2.2.3
36
- fix: sub_path '' => '/', like GitHub pages
47
- fix: redirect with no querystring

docs/guide/spa-server-configuration.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ file_dir = "/data"
4040
// # directory to store account and certificate
4141
// # optional, default is ${file_dir}/acme
4242
// // dir = "/data/acme"
43-
// # ci / stage / prod, default is prod
43+
// # ci / stage / prod, default is prod, ci is just for CI test with Pebble, don't use it.
4444
// //type = prod
4545
// }
4646

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "spa-server-doc",
3-
"version": "2.2.3",
3+
"version": "2.2.4",
44
"description": "This is for docs powered by VitePress",
55
"private": true,
66
"type": "module",

server/Cargo.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "spa-server"
3-
version = "2.2.3"
3+
version = "2.2.4"
44
edition = "2021"
55
authors = ["timzaak"]
66
license = "MIT"
@@ -78,6 +78,6 @@ anyhow = { version = "1.0", features = ["backtrace"] }
7878
# solve dir walk without recursion
7979
walkdir = "2.5"
8080
# time
81-
chrono = "0.4"
81+
chrono = { version = "0.4", features = ["serde"] }
8282
#make if let more easy
8383
if_chain = "1"

server/src/acme.rs

+43-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::time::Duration;
88

99
use anyhow::{anyhow, bail, Context};
1010
use base64::prelude::*;
11-
use chrono::{TimeZone, Utc};
11+
use chrono::{DateTime, TimeZone, Utc};
1212
use dashmap::DashMap;
1313
use delay_timer::prelude::{DelayTimer, Task, TaskBuilder};
1414
use if_chain::if_chain;
@@ -27,7 +27,7 @@ use tracing::{debug, error, info, warn};
2727
use walkdir::WalkDir;
2828

2929
use crate::config::{get_host_path_from_domain, ACMEConfig, ACMEType, Config};
30-
use crate::domain_storage::DomainStorage;
30+
use crate::domain_storage::{CertInfo, DomainStorage};
3131
use crate::tls::load_ssl_file;
3232

3333
const ACME_DIR: &str = "acme";
@@ -494,21 +494,58 @@ impl ACMEManager {
494494
.set_maximum_parallel_runnable_num(1)
495495
.spawn_routine(task)?)
496496
}
497-
//TODO: add it.
498497
pub async fn add_new_domain(&self, host: &str) {
499498
let _ = self
500499
.sender
501500
.send(RefreshDomainMessage(vec![host.to_string()]))
502501
.await;
503502
}
503+
504+
pub async fn get_cert_data(&self, host:Option<&String>) ->anyhow::Result<Vec<CertInfo>>{
505+
let result = match host {
506+
Some(host) => {
507+
match self.certificate_map.get(host) {
508+
Some(value) => {
509+
let [begin, end] = get_cert_validate_time(&value)?;
510+
vec![CertInfo {
511+
begin,
512+
end,
513+
host: host.to_owned(),
514+
}]
515+
516+
},
517+
None => vec![]
518+
}
519+
}
520+
None => {
521+
self.certificate_map.iter().filter_map(|item| {
522+
match get_cert_validate_time(&item) {
523+
Ok([begin, end]) => Some(CertInfo {
524+
begin, end, host:item.key().to_string()
525+
}),
526+
Err(error) => {
527+
warn!("get {} cert fail:{error}", item.key());
528+
None
529+
}
530+
}
531+
}).collect()
532+
}
533+
};
534+
Ok(result)
535+
}
536+
504537
}
505538

506-
fn cert_need_refresh(certified_key: &CertifiedKey) -> anyhow::Result<bool> {
539+
fn get_cert_validate_time(certified_key: &CertifiedKey) -> anyhow::Result<[DateTime<Utc>;2]> {
507540
let cert = certified_key.end_entity_cert()?;
508541
let (_, cert) = x509_parser::parse_x509_certificate(cert.as_ref())?;
509542
let validity = cert.validity();
510-
let [begin, end] = [validity.not_before, validity.not_after]
511-
.map(|t| Utc.timestamp_opt(t.timestamp(), 0).unwrap());
543+
Ok([validity.not_before, validity.not_after]
544+
.map(|t| Utc.timestamp_opt(t.timestamp(), 0).unwrap()))
545+
}
546+
547+
fn cert_need_refresh(certified_key: &CertifiedKey) -> anyhow::Result<bool> {
548+
let [begin, end] = get_cert_validate_time(certified_key)?;
512549
let now = Utc::now();
513550
Ok(now < begin || now > end || end - now < chrono::Duration::days(9))
514551
}

server/src/admin_server.rs

+33-15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::acme::ACMEManager;
1+
use crate::acme::ACMEManager;
22
use crate::admin_server::request::{
33
DeleteDomainVersionOption, DomainWithOptVersionOption, DomainWithVersionOption,
44
GetDomainOption, GetDomainPositionOption, UpdateUploadingStatusOption, UploadFileOption,
@@ -44,17 +44,18 @@ impl AdminServer {
4444

4545
fn routes(&self) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
4646
self.auth().and(
47-
(warp::get().and(
47+
warp::get().and(
4848
self.get_domain_info()
4949
.or(self.get_domain_upload_path())
50-
.or(self.get_files_metadata()),
51-
))
50+
.or(self.get_files_metadata())
51+
.or(self.get_acme_cert_info())
52+
)
5253
.or(warp::post().and(
5354
self.update_domain_version()
5455
.or(self.reload_server())
5556
.or(self.change_upload_status())
5657
.or(self.upload_file())
57-
.or(self.remove_domain_version()),
58+
.or(self.remove_domain_version())
5859
)),
5960
)
6061
}
@@ -180,6 +181,13 @@ impl AdminServer {
180181
.and(warp::query::<DeleteDomainVersionOption>())
181182
.map(service::remove_domain_version)
182183
}
184+
185+
fn get_acme_cert_info(&self) -> impl Filter<Extract = (impl warp::Reply,), Error = Rejection> + Clone {
186+
warp::path!("cert" / "acme")
187+
.and(with(self.acme_manager.clone()))
188+
.and(warp::query::<GetDomainOption>())
189+
.and_then(service::get_acme_cert_info)
190+
}
183191
}
184192

185193
pub mod service {
@@ -324,7 +332,7 @@ pub mod service {
324332
storage.save_file(query.domain, query.version, query.path, file)?;
325333
return Ok(Response::default());
326334
}
327-
return Err(anyhow!("bad params, please check the api doc: https://github.com/fornetcode/spa-server/blob/master/docs/guide/sap-server-api.md"));
335+
Err(anyhow!("bad params, please check the api doc: https://github.com/fornetcode/spa-server/blob/master/docs/guide/sap-server-api.md"))
328336
}
329337

330338
pub(super) fn get_files_metadata(
@@ -351,7 +359,7 @@ pub mod service {
351359
storage
352360
.get_domain_info_by_domain(&domain)
353361
.map(|v| vec![v])
354-
.unwrap_or(vec![])
362+
.unwrap_or_default()
355363
} else {
356364
storage.get_domain_info().unwrap_or_else(|_| vec![])
357365
};
@@ -362,7 +370,7 @@ pub mod service {
362370
.or(info.versions.iter().max().map(|x| *x))
363371
{
364372
//TODO: fix it, get reserve versions by array index compare, rather than -.
365-
max_version = max_version - max_reserve;
373+
max_version -= max_reserve;
366374
info.versions
367375
.into_iter()
368376
.filter(|v| *v < max_version)
@@ -384,6 +392,20 @@ pub mod service {
384392
}
385393
Response::default()
386394
}
395+
pub(super) async fn get_acme_cert_info(
396+
acme_manager: Arc<ACMEManager>,
397+
query: GetDomainOption,
398+
) -> Result<Response, Infallible> {
399+
let resp = match acme_manager.get_cert_data(query.domain.as_ref()).await {
400+
Ok(data) => warp::reply::json(&data).into_response(),
401+
Err(err) => {
402+
let mut resp = Response::new(Body::from(err.to_string()));
403+
*resp.status_mut() = StatusCode::BAD_REQUEST;
404+
resp
405+
}
406+
};
407+
Ok(resp)
408+
}
387409
}
388410

389411
pub mod request {
@@ -395,16 +417,12 @@ pub mod request {
395417
pub domain: Option<String>,
396418
}
397419

398-
#[derive(Deserialize, Serialize, Debug, Eq, PartialEq)]
420+
#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Default)]
399421
pub enum GetDomainPositionFormat {
422+
#[default]
400423
Path,
401424
Json,
402425
}
403-
impl Default for GetDomainPositionFormat {
404-
fn default() -> Self {
405-
GetDomainPositionFormat::Path
406-
}
407-
}
408426

409427
#[derive(Deserialize, Serialize, Debug)]
410428
pub struct GetDomainPositionOption {
@@ -492,7 +510,7 @@ mod test {
492510
assert!(task.is_valid());
493511
for _ in 0..10 {
494512
let time = task.get_next_exec_timestamp().unwrap() as i64;
495-
let time = NaiveDateTime::from_timestamp(time, 0);
513+
let time = NaiveDateTime::from_timestamp_opt(time, 0).unwrap();
496514
let time: DateTime<Utc> = DateTime::from_utc(time, Utc);
497515
println!("{}", time.format("%Y-%m-%d %H:%M:%S"));
498516
}

server/src/domain_storage.rs

+9
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::io::{Read, Write};
1313
use std::ops::RangeInclusive;
1414
use std::path::{Path, PathBuf};
1515
use std::sync::Arc;
16+
use chrono::{DateTime, Utc};
1617
use tracing::{debug, info};
1718
use walkdir::{DirEntry, WalkDir};
1819
use warp::fs::sanitize_path;
@@ -743,6 +744,14 @@ pub struct UploadDomainPosition {
743744
pub status: GetDomainPositionStatus,
744745
}
745746

747+
#[derive(Deserialize, Serialize, Debug)]
748+
pub struct CertInfo {
749+
pub begin: DateTime<Utc>,
750+
pub end: DateTime<Utc>,
751+
pub host: String,
752+
}
753+
754+
746755
#[cfg(test)]
747756
mod test {
748757
use crate::config::Config;

tests/tests/acme_test.rs

+26-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::fs;
33
use std::path::PathBuf;
44
use std::time::Duration;
55
use tokio::time::sleep;
6+
use spa_server::config::get_host_path_from_domain;
67

78
mod common;
89

@@ -30,14 +31,35 @@ async fn simple_acme_test() {
3031
sleep(Duration::from_secs(2)).await;
3132
upload_file_and_check(domain, request_prefix, 1, vec![]).await;
3233

33-
sleep(Duration::from_secs(10)).await;
34+
let (api, _) = get_client_api("client_config.conf");
35+
let mut wait_count =0;
36+
loop {
37+
assert!(wait_count < 20, "20 seconds doest not have cert");
38+
sleep(Duration::from_secs(1)).await;
39+
let cert_info = api.get_acme_cert_info(Some(get_host_path_from_domain(domain).0.to_string())).await.unwrap();
40+
if !cert_info.is_empty() {
41+
break
42+
}
43+
wait_count +=1;
44+
}
45+
3446
assert_files(domain, request_prefix, 1, vec!["", "index.html"]).await;
35-
// sometimes it output error. don't know why
3647
/*
48+
wait_count = 0;
3749
server.abort();
38-
sleep(Duration::from_secs(2)).await;
50+
println!("begin to loop server close");
51+
loop {
52+
assert!(wait_count < 20, "10 seconds server does not stop");
53+
sleep(Duration::from_secs(1)).await;
54+
let cert_info = api.get_domain_info(Some(get_host_path_from_domain(domain).0.to_string())).await;
55+
if cert_info.is_err() {
56+
break
57+
}
58+
wait_count +=1;
59+
}
60+
// sometimes it output error. don't know why
3961
run_server_with_config("server_config_acme.conf");
4062
sleep(Duration::from_secs(2)).await;
41-
assert_files(domain, request_prefix, 1, vec!["", "index.html"]).await;
63+
assert_files(domain, request_prefix, 1, vec!["", "index.html"]).await;
4264
*/
4365
}

0 commit comments

Comments
 (0)