itsi-scheduler 0.1.5 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Cargo.lock +120 -52
- data/README.md +57 -24
- data/Rakefile +0 -4
- data/ext/itsi_acme/Cargo.toml +86 -0
- data/ext/itsi_acme/examples/high_level.rs +63 -0
- data/ext/itsi_acme/examples/high_level_warp.rs +52 -0
- data/ext/itsi_acme/examples/low_level.rs +87 -0
- data/ext/itsi_acme/examples/low_level_axum.rs +66 -0
- data/ext/itsi_acme/src/acceptor.rs +81 -0
- data/ext/itsi_acme/src/acme.rs +354 -0
- data/ext/itsi_acme/src/axum.rs +86 -0
- data/ext/itsi_acme/src/cache.rs +39 -0
- data/ext/itsi_acme/src/caches/boxed.rs +80 -0
- data/ext/itsi_acme/src/caches/composite.rs +69 -0
- data/ext/itsi_acme/src/caches/dir.rs +106 -0
- data/ext/itsi_acme/src/caches/mod.rs +11 -0
- data/ext/itsi_acme/src/caches/no.rs +78 -0
- data/ext/itsi_acme/src/caches/test.rs +136 -0
- data/ext/itsi_acme/src/config.rs +172 -0
- data/ext/itsi_acme/src/https_helper.rs +69 -0
- data/ext/itsi_acme/src/incoming.rs +142 -0
- data/ext/itsi_acme/src/jose.rs +161 -0
- data/ext/itsi_acme/src/lib.rs +142 -0
- data/ext/itsi_acme/src/resolver.rs +59 -0
- data/ext/itsi_acme/src/state.rs +424 -0
- data/ext/itsi_error/Cargo.toml +1 -0
- data/ext/itsi_error/src/lib.rs +106 -7
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
- data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
- data/ext/itsi_rb_helpers/Cargo.toml +1 -0
- data/ext/itsi_rb_helpers/src/heap_value.rs +18 -0
- data/ext/itsi_rb_helpers/src/lib.rs +63 -12
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
- data/ext/itsi_rb_helpers/target/debug/build/rb-sys-eb9ed4ff3a60f995/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
- data/ext/itsi_scheduler/Cargo.toml +1 -1
- data/ext/itsi_scheduler/src/itsi_scheduler.rs +9 -3
- data/ext/itsi_scheduler/src/lib.rs +1 -0
- data/ext/itsi_server/Cargo.lock +2956 -0
- data/ext/itsi_server/Cargo.toml +73 -29
- data/ext/itsi_server/src/default_responses/mod.rs +11 -0
- data/ext/itsi_server/src/env.rs +43 -0
- data/ext/itsi_server/src/lib.rs +114 -75
- data/ext/itsi_server/src/prelude.rs +2 -0
- data/ext/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
- data/ext/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +29 -8
- data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +264 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +362 -0
- data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +84 -40
- data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +233 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +565 -0
- data/ext/itsi_server/src/ruby_types/itsi_server.rs +86 -0
- data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
- data/ext/itsi_server/src/server/{bind.rs → binds/bind.rs} +59 -24
- data/ext/itsi_server/src/server/binds/listener.rs +444 -0
- data/ext/itsi_server/src/server/binds/mod.rs +4 -0
- data/ext/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +57 -19
- data/ext/itsi_server/src/server/{tls.rs → binds/tls.rs} +120 -31
- data/ext/itsi_server/src/server/byte_frame.rs +32 -0
- data/ext/itsi_server/src/server/http_message_types.rs +97 -0
- data/ext/itsi_server/src/server/io_stream.rs +2 -1
- data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
- data/ext/itsi_server/src/server/middleware_stack/middleware.rs +170 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +63 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +94 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +94 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +343 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +151 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +316 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +301 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/csp.rs +193 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +64 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +192 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +171 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +198 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +209 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +116 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +411 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +142 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +55 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +54 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +51 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +126 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +187 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +173 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +31 -0
- data/ext/itsi_server/src/server/middleware_stack/mod.rs +381 -0
- data/ext/itsi_server/src/server/mod.rs +7 -5
- data/ext/itsi_server/src/server/process_worker.rs +65 -14
- data/ext/itsi_server/src/server/redirect_type.rs +26 -0
- data/ext/itsi_server/src/server/request_job.rs +11 -0
- data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +150 -50
- data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +399 -165
- data/ext/itsi_server/src/server/signal.rs +33 -26
- data/ext/itsi_server/src/server/size_limited_incoming.rs +107 -0
- data/ext/itsi_server/src/server/thread_worker.rs +218 -107
- data/ext/itsi_server/src/services/cache_store.rs +74 -0
- data/ext/itsi_server/src/services/itsi_http_service.rs +257 -0
- data/ext/itsi_server/src/services/mime_types.rs +1416 -0
- data/ext/itsi_server/src/services/mod.rs +6 -0
- data/ext/itsi_server/src/services/password_hasher.rs +83 -0
- data/ext/itsi_server/src/services/rate_limiter.rs +580 -0
- data/ext/itsi_server/src/services/static_file_server.rs +1340 -0
- data/ext/itsi_tracing/Cargo.toml +1 -0
- data/ext/itsi_tracing/src/lib.rs +362 -33
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
- data/itsi-scheduler-100.png +0 -0
- data/lib/itsi/scheduler/version.rb +1 -1
- data/lib/itsi/scheduler.rb +11 -6
- metadata +117 -24
- data/CHANGELOG.md +0 -5
- data/CODE_OF_CONDUCT.md +0 -132
- data/LICENSE.txt +0 -21
- data/ext/itsi_error/src/from.rs +0 -71
- data/ext/itsi_server/extconf.rb +0 -6
- data/ext/itsi_server/src/body_proxy/mod.rs +0 -2
- data/ext/itsi_server/src/request/itsi_request.rs +0 -277
- data/ext/itsi_server/src/request/mod.rs +0 -1
- data/ext/itsi_server/src/response/mod.rs +0 -1
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -13
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -5
- data/ext/itsi_server/src/server/itsi_server.rs +0 -244
- data/ext/itsi_server/src/server/listener.rs +0 -327
- /data/ext/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
use clap::Parser;
|
2
|
+
use itsi_acme::caches::DirCache;
|
3
|
+
use itsi_acme::AcmeConfig;
|
4
|
+
use std::net::Ipv6Addr;
|
5
|
+
use std::path::PathBuf;
|
6
|
+
use tokio::io::AsyncWriteExt;
|
7
|
+
use tokio_stream::wrappers::TcpListenerStream;
|
8
|
+
use tokio_stream::StreamExt;
|
9
|
+
|
10
|
+
#[derive(Parser, Debug)]
|
11
|
+
struct Args {
|
12
|
+
/// Domains
|
13
|
+
#[clap(short, required = true)]
|
14
|
+
domains: Vec<String>,
|
15
|
+
|
16
|
+
/// Contact info
|
17
|
+
#[clap(short)]
|
18
|
+
email: Vec<String>,
|
19
|
+
|
20
|
+
/// Cache directory
|
21
|
+
#[clap(short)]
|
22
|
+
cache: Option<PathBuf>,
|
23
|
+
|
24
|
+
/// Use Let's Encrypt production environment
|
25
|
+
/// (see https://letsencrypt.org/docs/staging-environment/)
|
26
|
+
#[clap(long)]
|
27
|
+
prod: bool,
|
28
|
+
|
29
|
+
#[clap(short, long, default_value = "443")]
|
30
|
+
port: u16,
|
31
|
+
}
|
32
|
+
|
33
|
+
#[tokio::main]
|
34
|
+
async fn main() {
|
35
|
+
simple_logger::init_with_level(log::Level::Info).unwrap();
|
36
|
+
let args = Args::parse();
|
37
|
+
|
38
|
+
let tcp_listener = tokio::net::TcpListener::bind((Ipv6Addr::UNSPECIFIED, args.port))
|
39
|
+
.await
|
40
|
+
.unwrap();
|
41
|
+
let tcp_incoming = TcpListenerStream::new(tcp_listener);
|
42
|
+
|
43
|
+
let mut tls_incoming = AcmeConfig::new(args.domains)
|
44
|
+
.contact(args.email.iter().map(|e| format!("mailto:{}", e)))
|
45
|
+
.cache_option(args.cache.clone().map(DirCache::new))
|
46
|
+
.directory_lets_encrypt(args.prod)
|
47
|
+
.incoming(tcp_incoming, Vec::new());
|
48
|
+
|
49
|
+
while let Some(tls) = tls_incoming.next().await {
|
50
|
+
let mut tls = tls.unwrap();
|
51
|
+
tokio::spawn(async move {
|
52
|
+
tls.write_all(HELLO).await.unwrap();
|
53
|
+
tls.shutdown().await.unwrap();
|
54
|
+
});
|
55
|
+
}
|
56
|
+
unreachable!()
|
57
|
+
}
|
58
|
+
|
59
|
+
const HELLO: &[u8] = br#"HTTP/1.1 200 OK
|
60
|
+
Content-Length: 10
|
61
|
+
Content-Type: text/plain; charset=utf-8
|
62
|
+
|
63
|
+
Hello Tls!"#;
|
@@ -0,0 +1,52 @@
|
|
1
|
+
use clap::Parser;
|
2
|
+
use itsi_acme::caches::DirCache;
|
3
|
+
use itsi_acme::AcmeConfig;
|
4
|
+
use std::net::Ipv6Addr;
|
5
|
+
use std::path::PathBuf;
|
6
|
+
use tokio_stream::wrappers::TcpListenerStream;
|
7
|
+
use warp::Filter;
|
8
|
+
|
9
|
+
#[derive(Parser, Debug)]
|
10
|
+
struct Args {
|
11
|
+
/// Domains
|
12
|
+
#[clap(short, required = true)]
|
13
|
+
domains: Vec<String>,
|
14
|
+
|
15
|
+
/// Contact info
|
16
|
+
#[clap(short)]
|
17
|
+
email: Vec<String>,
|
18
|
+
|
19
|
+
/// Cache directory
|
20
|
+
#[clap(short)]
|
21
|
+
cache: Option<PathBuf>,
|
22
|
+
|
23
|
+
/// Use Let's Encrypt production environment
|
24
|
+
/// (see https://letsencrypt.org/docs/staging-environment/)
|
25
|
+
#[clap(long)]
|
26
|
+
prod: bool,
|
27
|
+
|
28
|
+
#[clap(short, long, default_value = "443")]
|
29
|
+
port: u16,
|
30
|
+
}
|
31
|
+
|
32
|
+
#[tokio::main]
|
33
|
+
async fn main() {
|
34
|
+
simple_logger::init_with_level(log::Level::Info).unwrap();
|
35
|
+
let args = Args::parse();
|
36
|
+
|
37
|
+
let tcp_listener = tokio::net::TcpListener::bind((Ipv6Addr::UNSPECIFIED, args.port))
|
38
|
+
.await
|
39
|
+
.unwrap();
|
40
|
+
let tcp_incoming = TcpListenerStream::new(tcp_listener);
|
41
|
+
|
42
|
+
let tls_incoming = AcmeConfig::new(args.domains)
|
43
|
+
.contact(args.email.iter().map(|e| format!("mailto:{}", e)))
|
44
|
+
.cache_option(args.cache.clone().map(DirCache::new))
|
45
|
+
.directory_lets_encrypt(args.prod)
|
46
|
+
.incoming(tcp_incoming, Vec::new());
|
47
|
+
|
48
|
+
let route = warp::any().map(|| "Hello Tls!");
|
49
|
+
warp::serve(route).run_incoming(tls_incoming).await;
|
50
|
+
|
51
|
+
unreachable!()
|
52
|
+
}
|
@@ -0,0 +1,87 @@
|
|
1
|
+
use clap::Parser;
|
2
|
+
use itsi_acme::caches::DirCache;
|
3
|
+
use itsi_acme::{AcmeAcceptor, AcmeConfig};
|
4
|
+
use rustls::ServerConfig;
|
5
|
+
use std::net::Ipv6Addr;
|
6
|
+
use std::path::PathBuf;
|
7
|
+
use std::sync::Arc;
|
8
|
+
use tokio::io::AsyncWriteExt;
|
9
|
+
use tokio_stream::StreamExt;
|
10
|
+
|
11
|
+
#[derive(Parser, Debug)]
|
12
|
+
struct Args {
|
13
|
+
/// Domains
|
14
|
+
#[clap(short, required = true)]
|
15
|
+
domains: Vec<String>,
|
16
|
+
|
17
|
+
/// Contact info
|
18
|
+
#[clap(short)]
|
19
|
+
email: Vec<String>,
|
20
|
+
|
21
|
+
/// Cache directory
|
22
|
+
#[clap(short)]
|
23
|
+
cache: Option<PathBuf>,
|
24
|
+
|
25
|
+
/// Use Let's Encrypt production environment
|
26
|
+
/// (see https://letsencrypt.org/docs/staging-environment/)
|
27
|
+
#[clap(long)]
|
28
|
+
prod: bool,
|
29
|
+
|
30
|
+
#[clap(short, long, default_value = "443")]
|
31
|
+
port: u16,
|
32
|
+
}
|
33
|
+
|
34
|
+
#[tokio::main]
|
35
|
+
async fn main() {
|
36
|
+
simple_logger::init_with_level(log::Level::Info).unwrap();
|
37
|
+
let args = Args::parse();
|
38
|
+
|
39
|
+
let mut state = AcmeConfig::new(args.domains)
|
40
|
+
.contact(args.email.iter().map(|e| format!("mailto:{}", e)))
|
41
|
+
.cache_option(args.cache.clone().map(DirCache::new))
|
42
|
+
.directory_lets_encrypt(args.prod)
|
43
|
+
.state();
|
44
|
+
let rustls_config = ServerConfig::builder()
|
45
|
+
.with_no_client_auth()
|
46
|
+
.with_cert_resolver(state.resolver());
|
47
|
+
let acceptor = state.acceptor();
|
48
|
+
|
49
|
+
tokio::spawn(async move {
|
50
|
+
loop {
|
51
|
+
match state.next().await.unwrap() {
|
52
|
+
Ok(ok) => log::info!("event: {:?}", ok),
|
53
|
+
Err(err) => log::error!("error: {:?}", err),
|
54
|
+
}
|
55
|
+
}
|
56
|
+
});
|
57
|
+
|
58
|
+
serve(acceptor, Arc::new(rustls_config), args.port).await;
|
59
|
+
}
|
60
|
+
|
61
|
+
async fn serve(acceptor: AcmeAcceptor, rustls_config: Arc<ServerConfig>, port: u16) {
|
62
|
+
let listener = tokio::net::TcpListener::bind((Ipv6Addr::UNSPECIFIED, port))
|
63
|
+
.await
|
64
|
+
.unwrap();
|
65
|
+
loop {
|
66
|
+
let tcp = listener.accept().await.unwrap().0;
|
67
|
+
let rustls_config = rustls_config.clone();
|
68
|
+
let accept_future = acceptor.accept(tcp);
|
69
|
+
|
70
|
+
tokio::spawn(async move {
|
71
|
+
match accept_future.await.unwrap() {
|
72
|
+
None => log::info!("received TLS-ALPN-01 validation request"),
|
73
|
+
Some(start_handshake) => {
|
74
|
+
let mut tls = start_handshake.into_stream(rustls_config).await.unwrap();
|
75
|
+
tls.write_all(HELLO).await.unwrap();
|
76
|
+
tls.shutdown().await.unwrap();
|
77
|
+
}
|
78
|
+
}
|
79
|
+
});
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
const HELLO: &[u8] = br#"HTTP/1.1 200 OK
|
84
|
+
Content-Length: 10
|
85
|
+
Content-Type: text/plain; charset=utf-8
|
86
|
+
|
87
|
+
Hello Tls!"#;
|
@@ -0,0 +1,66 @@
|
|
1
|
+
use axum::{routing::get, Router};
|
2
|
+
use clap::Parser;
|
3
|
+
use itsi_acme::caches::DirCache;
|
4
|
+
use itsi_acme::AcmeConfig;
|
5
|
+
use rustls::ServerConfig;
|
6
|
+
use std::net::{Ipv6Addr, SocketAddr};
|
7
|
+
use std::path::PathBuf;
|
8
|
+
use std::sync::Arc;
|
9
|
+
use tokio_stream::StreamExt;
|
10
|
+
|
11
|
+
#[derive(Parser, Debug)]
|
12
|
+
struct Args {
|
13
|
+
/// Domains
|
14
|
+
#[clap(short, required = true)]
|
15
|
+
domains: Vec<String>,
|
16
|
+
|
17
|
+
/// Contact info
|
18
|
+
#[clap(short)]
|
19
|
+
email: Vec<String>,
|
20
|
+
|
21
|
+
/// Cache directory
|
22
|
+
#[clap(short)]
|
23
|
+
cache: Option<PathBuf>,
|
24
|
+
|
25
|
+
/// Use Let's Encrypt production environment
|
26
|
+
/// (see https://letsencrypt.org/docs/staging-environment/)
|
27
|
+
#[clap(long)]
|
28
|
+
prod: bool,
|
29
|
+
|
30
|
+
#[clap(short, long, default_value = "443")]
|
31
|
+
port: u16,
|
32
|
+
}
|
33
|
+
|
34
|
+
#[tokio::main]
|
35
|
+
async fn main() {
|
36
|
+
simple_logger::init_with_level(log::Level::Info).unwrap();
|
37
|
+
let args = Args::parse();
|
38
|
+
|
39
|
+
let mut state = AcmeConfig::new(args.domains)
|
40
|
+
.contact(args.email.iter().map(|e| format!("mailto:{}", e)))
|
41
|
+
.cache_option(args.cache.clone().map(DirCache::new))
|
42
|
+
.directory_lets_encrypt(args.prod)
|
43
|
+
.state();
|
44
|
+
let rustls_config = ServerConfig::builder()
|
45
|
+
.with_no_client_auth()
|
46
|
+
.with_cert_resolver(state.resolver());
|
47
|
+
let acceptor = state.axum_acceptor(Arc::new(rustls_config));
|
48
|
+
|
49
|
+
tokio::spawn(async move {
|
50
|
+
loop {
|
51
|
+
match state.next().await.unwrap() {
|
52
|
+
Ok(ok) => log::info!("event: {:?}", ok),
|
53
|
+
Err(err) => log::error!("error: {:?}", err),
|
54
|
+
}
|
55
|
+
}
|
56
|
+
});
|
57
|
+
|
58
|
+
let app = Router::new().route("/", get(|| async { "Hello Tls!" }));
|
59
|
+
|
60
|
+
let addr = SocketAddr::from((Ipv6Addr::UNSPECIFIED, args.port));
|
61
|
+
axum_server::bind(addr)
|
62
|
+
.acceptor(acceptor)
|
63
|
+
.serve(app.into_make_service())
|
64
|
+
.await
|
65
|
+
.unwrap();
|
66
|
+
}
|
@@ -0,0 +1,81 @@
|
|
1
|
+
use crate::acme::ACME_TLS_ALPN_NAME;
|
2
|
+
use crate::ResolvesServerCertAcme;
|
3
|
+
use rustls::server::Acceptor;
|
4
|
+
use rustls::ServerConfig;
|
5
|
+
use std::future::Future;
|
6
|
+
use std::io;
|
7
|
+
use std::pin::Pin;
|
8
|
+
use std::sync::Arc;
|
9
|
+
use std::task::{Context, Poll};
|
10
|
+
use tokio::io::{AsyncRead, AsyncWrite};
|
11
|
+
use tokio_rustls::{Accept, LazyConfigAcceptor, StartHandshake};
|
12
|
+
|
13
|
+
#[derive(Clone)]
|
14
|
+
pub struct AcmeAcceptor {
|
15
|
+
config: Arc<ServerConfig>,
|
16
|
+
}
|
17
|
+
|
18
|
+
impl AcmeAcceptor {
|
19
|
+
pub(crate) fn new(resolver: Arc<ResolvesServerCertAcme>) -> Self {
|
20
|
+
let mut config = ServerConfig::builder()
|
21
|
+
.with_no_client_auth()
|
22
|
+
.with_cert_resolver(resolver);
|
23
|
+
config.alpn_protocols.push(ACME_TLS_ALPN_NAME.to_vec());
|
24
|
+
Self {
|
25
|
+
config: Arc::new(config),
|
26
|
+
}
|
27
|
+
}
|
28
|
+
pub fn accept<IO: AsyncRead + AsyncWrite + Unpin>(&self, io: IO) -> AcmeAccept<IO> {
|
29
|
+
AcmeAccept::new(io, self.config.clone())
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
pub struct AcmeAccept<IO: AsyncRead + AsyncWrite + Unpin> {
|
34
|
+
acceptor: LazyConfigAcceptor<IO>,
|
35
|
+
config: Arc<ServerConfig>,
|
36
|
+
validation_accept: Option<Accept<IO>>,
|
37
|
+
}
|
38
|
+
|
39
|
+
impl<IO: AsyncRead + AsyncWrite + Unpin> AcmeAccept<IO> {
|
40
|
+
pub(crate) fn new(io: IO, config: Arc<ServerConfig>) -> Self {
|
41
|
+
Self {
|
42
|
+
acceptor: LazyConfigAcceptor::new(Acceptor::default(), io),
|
43
|
+
config,
|
44
|
+
validation_accept: None,
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
impl<IO: AsyncRead + AsyncWrite + Unpin> Future for AcmeAccept<IO> {
|
50
|
+
type Output = io::Result<Option<StartHandshake<IO>>>;
|
51
|
+
|
52
|
+
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
53
|
+
loop {
|
54
|
+
if let Some(validation_accept) = &mut self.validation_accept {
|
55
|
+
return match Pin::new(validation_accept).poll(cx) {
|
56
|
+
Poll::Ready(Ok(_)) => Poll::Ready(Ok(None)),
|
57
|
+
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
|
58
|
+
Poll::Pending => Poll::Pending,
|
59
|
+
};
|
60
|
+
}
|
61
|
+
|
62
|
+
return match Pin::new(&mut self.acceptor).poll(cx) {
|
63
|
+
Poll::Ready(Ok(handshake)) => {
|
64
|
+
let is_validation = handshake
|
65
|
+
.client_hello()
|
66
|
+
.alpn()
|
67
|
+
.into_iter()
|
68
|
+
.flatten()
|
69
|
+
.eq([ACME_TLS_ALPN_NAME]);
|
70
|
+
if is_validation {
|
71
|
+
self.validation_accept = Some(handshake.into_stream(self.config.clone()));
|
72
|
+
continue;
|
73
|
+
}
|
74
|
+
Poll::Ready(Ok(Some(handshake)))
|
75
|
+
}
|
76
|
+
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
|
77
|
+
Poll::Pending => Poll::Pending,
|
78
|
+
};
|
79
|
+
}
|
80
|
+
}
|
81
|
+
}
|
@@ -0,0 +1,354 @@
|
|
1
|
+
use std::sync::Arc;
|
2
|
+
|
3
|
+
use crate::https_helper::{https, HttpsRequestError, Method, Response};
|
4
|
+
use crate::jose::{key_authorization_sha256, sign, sign_eab, JoseError};
|
5
|
+
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
6
|
+
use base64::Engine;
|
7
|
+
use rcgen::{CustomExtension, Error as RcgenError, PKCS_ECDSA_P256_SHA256};
|
8
|
+
use ring::error::{KeyRejected, Unspecified};
|
9
|
+
use ring::rand::SystemRandom;
|
10
|
+
use ring::signature::{EcdsaKeyPair, EcdsaSigningAlgorithm, ECDSA_P256_SHA256_FIXED_SIGNING};
|
11
|
+
use rustls::{crypto::ring::sign::any_ecdsa_type, sign::CertifiedKey};
|
12
|
+
use rustls::{
|
13
|
+
pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer},
|
14
|
+
ClientConfig,
|
15
|
+
};
|
16
|
+
use serde::{Deserialize, Serialize};
|
17
|
+
use serde_json::json;
|
18
|
+
use thiserror::Error;
|
19
|
+
|
20
|
+
pub const LETS_ENCRYPT_STAGING_DIRECTORY: &str =
|
21
|
+
"https://acme-staging-v02.api.letsencrypt.org/directory";
|
22
|
+
pub const LETS_ENCRYPT_PRODUCTION_DIRECTORY: &str =
|
23
|
+
"https://acme-v02.api.letsencrypt.org/directory";
|
24
|
+
pub const ACME_TLS_ALPN_NAME: &[u8] = b"acme-tls/1";
|
25
|
+
|
26
|
+
#[derive(Debug)]
|
27
|
+
pub struct Account {
|
28
|
+
pub key_pair: EcdsaKeyPair,
|
29
|
+
pub directory: Directory,
|
30
|
+
pub kid: String,
|
31
|
+
}
|
32
|
+
|
33
|
+
static ALG: &EcdsaSigningAlgorithm = &ECDSA_P256_SHA256_FIXED_SIGNING;
|
34
|
+
|
35
|
+
impl Account {
|
36
|
+
pub fn generate_key_pair() -> Vec<u8> {
|
37
|
+
let rng = SystemRandom::new();
|
38
|
+
let pkcs8 = EcdsaKeyPair::generate_pkcs8(ALG, &rng).unwrap();
|
39
|
+
pkcs8.as_ref().to_vec()
|
40
|
+
}
|
41
|
+
pub async fn create<'a, S, I>(
|
42
|
+
client_config: &Arc<ClientConfig>,
|
43
|
+
directory: Directory,
|
44
|
+
contact: I,
|
45
|
+
eab: &Option<ExternalAccountKey>,
|
46
|
+
) -> Result<Self, AcmeError>
|
47
|
+
where
|
48
|
+
S: AsRef<str> + 'a,
|
49
|
+
I: IntoIterator<Item = &'a S>,
|
50
|
+
{
|
51
|
+
let key_pair = Self::generate_key_pair();
|
52
|
+
Self::create_with_keypair(client_config, directory, contact, &key_pair, eab).await
|
53
|
+
}
|
54
|
+
pub async fn create_with_keypair<'a, S, I>(
|
55
|
+
client_config: &Arc<ClientConfig>,
|
56
|
+
directory: Directory,
|
57
|
+
contact: I,
|
58
|
+
key_pair: &[u8],
|
59
|
+
eab: &Option<ExternalAccountKey>,
|
60
|
+
) -> Result<Self, AcmeError>
|
61
|
+
where
|
62
|
+
S: AsRef<str> + 'a,
|
63
|
+
I: IntoIterator<Item = &'a S>,
|
64
|
+
{
|
65
|
+
let key_pair = EcdsaKeyPair::from_pkcs8(ALG, key_pair, &SystemRandom::new())?;
|
66
|
+
let contact: Vec<&'a str> = contact.into_iter().map(AsRef::<str>::as_ref).collect();
|
67
|
+
|
68
|
+
let payload = if let Some(eab) = &eab.as_ref() {
|
69
|
+
let eab_body = sign_eab(&key_pair, &eab.key, &eab.kid, &directory.new_account)?;
|
70
|
+
|
71
|
+
json!({
|
72
|
+
"termsOfServiceAgreed": true,
|
73
|
+
"contact": contact,
|
74
|
+
"externalAccountBinding": eab_body,
|
75
|
+
})
|
76
|
+
} else {
|
77
|
+
json!({
|
78
|
+
"termsOfServiceAgreed": true,
|
79
|
+
"contact": contact,
|
80
|
+
})
|
81
|
+
}
|
82
|
+
.to_string();
|
83
|
+
|
84
|
+
let body = sign(
|
85
|
+
&key_pair,
|
86
|
+
None,
|
87
|
+
directory.nonce(client_config).await?,
|
88
|
+
&directory.new_account,
|
89
|
+
&payload,
|
90
|
+
)?;
|
91
|
+
let response = https(
|
92
|
+
client_config,
|
93
|
+
&directory.new_account,
|
94
|
+
Method::Post,
|
95
|
+
Some(body),
|
96
|
+
)
|
97
|
+
.await?;
|
98
|
+
let kid = get_header(&response, "Location")?;
|
99
|
+
Ok(Account {
|
100
|
+
key_pair,
|
101
|
+
kid,
|
102
|
+
directory,
|
103
|
+
})
|
104
|
+
}
|
105
|
+
async fn request(
|
106
|
+
&self,
|
107
|
+
client_config: &Arc<ClientConfig>,
|
108
|
+
url: impl AsRef<str>,
|
109
|
+
payload: &str,
|
110
|
+
) -> Result<(Option<String>, String), AcmeError> {
|
111
|
+
let body = sign(
|
112
|
+
&self.key_pair,
|
113
|
+
Some(&self.kid),
|
114
|
+
self.directory.nonce(client_config).await?,
|
115
|
+
url.as_ref(),
|
116
|
+
payload,
|
117
|
+
)?;
|
118
|
+
let response = https(client_config, url.as_ref(), Method::Post, Some(body)).await?;
|
119
|
+
let location = get_header(&response, "Location").ok();
|
120
|
+
let body = response.text().await.map_err(HttpsRequestError::from)?;
|
121
|
+
log::debug!("response: {:?}", body);
|
122
|
+
Ok((location, body))
|
123
|
+
}
|
124
|
+
pub async fn new_order(
|
125
|
+
&self,
|
126
|
+
client_config: &Arc<ClientConfig>,
|
127
|
+
domains: Vec<String>,
|
128
|
+
) -> Result<(String, Order), AcmeError> {
|
129
|
+
let domains: Vec<Identifier> = domains.into_iter().map(Identifier::Dns).collect();
|
130
|
+
let payload = format!("{{\"identifiers\":{}}}", serde_json::to_string(&domains)?);
|
131
|
+
let response = self
|
132
|
+
.request(client_config, &self.directory.new_order, &payload)
|
133
|
+
.await?;
|
134
|
+
let url = response.0.ok_or(AcmeError::MissingHeader("Location"))?;
|
135
|
+
let order = serde_json::from_str(&response.1)?;
|
136
|
+
Ok((url, order))
|
137
|
+
}
|
138
|
+
pub async fn auth(
|
139
|
+
&self,
|
140
|
+
client_config: &Arc<ClientConfig>,
|
141
|
+
url: impl AsRef<str>,
|
142
|
+
) -> Result<Auth, AcmeError> {
|
143
|
+
let payload = "".to_string();
|
144
|
+
let response = self.request(client_config, url, &payload).await?;
|
145
|
+
Ok(serde_json::from_str(&response.1)?)
|
146
|
+
}
|
147
|
+
pub async fn challenge(
|
148
|
+
&self,
|
149
|
+
client_config: &Arc<ClientConfig>,
|
150
|
+
url: impl AsRef<str>,
|
151
|
+
) -> Result<(), AcmeError> {
|
152
|
+
self.request(client_config, &url, "{}").await?;
|
153
|
+
Ok(())
|
154
|
+
}
|
155
|
+
pub async fn order(
|
156
|
+
&self,
|
157
|
+
client_config: &Arc<ClientConfig>,
|
158
|
+
url: impl AsRef<str>,
|
159
|
+
) -> Result<Order, AcmeError> {
|
160
|
+
let response = self.request(client_config, &url, "").await?;
|
161
|
+
Ok(serde_json::from_str(&response.1)?)
|
162
|
+
}
|
163
|
+
pub async fn finalize(
|
164
|
+
&self,
|
165
|
+
client_config: &Arc<ClientConfig>,
|
166
|
+
url: impl AsRef<str>,
|
167
|
+
csr: Vec<u8>,
|
168
|
+
) -> Result<Order, AcmeError> {
|
169
|
+
let payload = format!("{{\"csr\":\"{}\"}}", URL_SAFE_NO_PAD.encode(csr),);
|
170
|
+
let response = self.request(client_config, &url, &payload).await?;
|
171
|
+
Ok(serde_json::from_str(&response.1)?)
|
172
|
+
}
|
173
|
+
pub async fn certificate(
|
174
|
+
&self,
|
175
|
+
client_config: &Arc<ClientConfig>,
|
176
|
+
url: impl AsRef<str>,
|
177
|
+
) -> Result<String, AcmeError> {
|
178
|
+
Ok(self.request(client_config, &url, "").await?.1)
|
179
|
+
}
|
180
|
+
pub fn tls_alpn_01<'a>(
|
181
|
+
&self,
|
182
|
+
challenges: &'a [Challenge],
|
183
|
+
domain: String,
|
184
|
+
) -> Result<(&'a Challenge, CertifiedKey), AcmeError> {
|
185
|
+
let challenge = challenges
|
186
|
+
.iter()
|
187
|
+
.find(|c| c.typ == ChallengeType::TlsAlpn01);
|
188
|
+
|
189
|
+
let challenge = match challenge {
|
190
|
+
Some(challenge) => challenge,
|
191
|
+
None => return Err(AcmeError::NoTlsAlpn01Challenge),
|
192
|
+
};
|
193
|
+
let mut params = rcgen::CertificateParams::new(vec![domain])?;
|
194
|
+
let key_auth = key_authorization_sha256(&self.key_pair, &challenge.token)?;
|
195
|
+
params.custom_extensions = vec![CustomExtension::new_acme_identifier(key_auth.as_ref())];
|
196
|
+
|
197
|
+
let key_pair = rcgen::KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?;
|
198
|
+
let cert = params.self_signed(&key_pair)?;
|
199
|
+
|
200
|
+
let pk_bytes = key_pair.serialize_der();
|
201
|
+
let pk_der: PrivatePkcs8KeyDer = pk_bytes.into();
|
202
|
+
let pk_der: PrivateKeyDer = pk_der.into();
|
203
|
+
let pk = any_ecdsa_type(&pk_der).unwrap();
|
204
|
+
let certified_key = CertifiedKey::new(vec![cert.der().clone()], pk);
|
205
|
+
Ok((challenge, certified_key))
|
206
|
+
}
|
207
|
+
}
|
208
|
+
|
209
|
+
#[derive(Debug, Clone, Deserialize)]
|
210
|
+
#[serde(rename_all = "camelCase")]
|
211
|
+
pub struct Directory {
|
212
|
+
pub new_nonce: String,
|
213
|
+
pub new_account: String,
|
214
|
+
pub new_order: String,
|
215
|
+
}
|
216
|
+
|
217
|
+
impl Directory {
|
218
|
+
pub async fn discover(
|
219
|
+
client_config: &Arc<ClientConfig>,
|
220
|
+
url: impl AsRef<str>,
|
221
|
+
) -> Result<Self, AcmeError> {
|
222
|
+
let response = https(client_config, url, Method::Get, None).await?;
|
223
|
+
let body = response.bytes().await.map_err(HttpsRequestError::from)?;
|
224
|
+
|
225
|
+
Ok(serde_json::from_slice(&body)?)
|
226
|
+
}
|
227
|
+
pub async fn nonce(&self, client_config: &Arc<ClientConfig>) -> Result<String, AcmeError> {
|
228
|
+
let response = &https(client_config, &self.new_nonce.as_str(), Method::Head, None).await?;
|
229
|
+
get_header(response, "replay-nonce")
|
230
|
+
}
|
231
|
+
}
|
232
|
+
|
233
|
+
/// See RFC 8555 section 7.3.4 for more information.
|
234
|
+
pub struct ExternalAccountKey {
|
235
|
+
pub kid: String,
|
236
|
+
pub key: ring::hmac::Key,
|
237
|
+
}
|
238
|
+
|
239
|
+
impl ExternalAccountKey {
|
240
|
+
pub fn new(kid: String, key: &[u8]) -> Self {
|
241
|
+
Self {
|
242
|
+
kid,
|
243
|
+
key: ring::hmac::Key::new(ring::hmac::HMAC_SHA256, key),
|
244
|
+
}
|
245
|
+
}
|
246
|
+
}
|
247
|
+
|
248
|
+
#[derive(Debug, Deserialize, Eq, PartialEq)]
|
249
|
+
pub enum ChallengeType {
|
250
|
+
#[serde(rename = "http-01")]
|
251
|
+
Http01,
|
252
|
+
#[serde(rename = "dns-01")]
|
253
|
+
Dns01,
|
254
|
+
#[serde(rename = "tls-alpn-01")]
|
255
|
+
TlsAlpn01,
|
256
|
+
}
|
257
|
+
|
258
|
+
#[derive(Debug, Deserialize)]
|
259
|
+
#[serde(rename_all = "camelCase")]
|
260
|
+
pub struct Order {
|
261
|
+
#[serde(flatten)]
|
262
|
+
pub status: OrderStatus,
|
263
|
+
pub authorizations: Vec<String>,
|
264
|
+
pub finalize: String,
|
265
|
+
pub error: Option<Problem>,
|
266
|
+
}
|
267
|
+
|
268
|
+
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
|
269
|
+
#[serde(tag = "status", rename_all = "camelCase")]
|
270
|
+
pub enum OrderStatus {
|
271
|
+
Pending,
|
272
|
+
Ready,
|
273
|
+
Valid { certificate: String },
|
274
|
+
Invalid,
|
275
|
+
Processing,
|
276
|
+
}
|
277
|
+
|
278
|
+
#[derive(Debug, Deserialize)]
|
279
|
+
#[serde(rename_all = "camelCase")]
|
280
|
+
pub struct Auth {
|
281
|
+
pub status: AuthStatus,
|
282
|
+
pub identifier: Identifier,
|
283
|
+
pub challenges: Vec<Challenge>,
|
284
|
+
}
|
285
|
+
|
286
|
+
#[derive(Debug, Deserialize)]
|
287
|
+
#[serde(rename_all = "camelCase")]
|
288
|
+
pub enum AuthStatus {
|
289
|
+
Pending,
|
290
|
+
Valid,
|
291
|
+
Invalid,
|
292
|
+
Revoked,
|
293
|
+
Expired,
|
294
|
+
Deactivated,
|
295
|
+
}
|
296
|
+
|
297
|
+
#[derive(Clone, Debug, Serialize, Deserialize)]
|
298
|
+
#[serde(tag = "type", content = "value", rename_all = "camelCase")]
|
299
|
+
pub enum Identifier {
|
300
|
+
Dns(String),
|
301
|
+
}
|
302
|
+
|
303
|
+
#[derive(Debug, Deserialize)]
|
304
|
+
pub struct Challenge {
|
305
|
+
#[serde(rename = "type")]
|
306
|
+
pub typ: ChallengeType,
|
307
|
+
pub url: String,
|
308
|
+
pub token: String,
|
309
|
+
pub error: Option<Problem>,
|
310
|
+
}
|
311
|
+
|
312
|
+
#[derive(Clone, Debug, Serialize, Deserialize)]
|
313
|
+
#[serde(rename_all = "camelCase")]
|
314
|
+
pub struct Problem {
|
315
|
+
#[serde(rename = "type")]
|
316
|
+
pub typ: Option<String>,
|
317
|
+
pub detail: Option<String>,
|
318
|
+
}
|
319
|
+
|
320
|
+
#[derive(Error, Debug)]
|
321
|
+
pub enum AcmeError {
|
322
|
+
#[error("io error: {0}")]
|
323
|
+
Io(#[from] std::io::Error),
|
324
|
+
#[error("certificate generation error: {0}")]
|
325
|
+
Rcgen(#[from] RcgenError),
|
326
|
+
#[error("JOSE error: {0}")]
|
327
|
+
Jose(#[from] JoseError),
|
328
|
+
#[error("JSON error: {0}")]
|
329
|
+
Json(#[from] serde_json::Error),
|
330
|
+
#[error("http request error: {0}")]
|
331
|
+
HttpRequest(#[from] HttpsRequestError),
|
332
|
+
#[error("invalid key pair: {0}")]
|
333
|
+
KeyRejected(#[from] KeyRejected),
|
334
|
+
#[error("crypto error: {0}")]
|
335
|
+
Crypto(#[from] Unspecified),
|
336
|
+
#[error("acme service response is missing {0} header")]
|
337
|
+
MissingHeader(&'static str),
|
338
|
+
#[error("no tls-alpn-01 challenge found")]
|
339
|
+
NoTlsAlpn01Challenge,
|
340
|
+
}
|
341
|
+
|
342
|
+
fn get_header(response: &Response, header: &'static str) -> Result<String, AcmeError> {
|
343
|
+
let h = response
|
344
|
+
.headers()
|
345
|
+
.get_all(header)
|
346
|
+
.iter()
|
347
|
+
.next_back()
|
348
|
+
.and_then(|v| v.to_str().ok())
|
349
|
+
.map(|s| s.to_string());
|
350
|
+
match h {
|
351
|
+
None => Err(AcmeError::MissingHeader(header)),
|
352
|
+
Some(value) => Ok(value),
|
353
|
+
}
|
354
|
+
}
|