Build or update the BlueBubbles external channel plugin for Moltbot (extension package, REST...
npx skills add salvo-rs/salvo-skills --skill "salvo-tls-acme"
Install specific skill from multi-skill repository
# Description
Configure TLS/HTTPS with automatic certificate management via ACME (Let's Encrypt). Use for production deployments with secure connections.
# SKILL.md
name: salvo-tls-acme
description: Configure TLS/HTTPS with automatic certificate management via ACME (Let's Encrypt). Use for production deployments with secure connections.
Salvo TLS and ACME Configuration
This skill helps configure TLS/HTTPS and automatic certificate management in Salvo applications.
TLS with Rustls
Setup
[dependencies]
salvo = { version = "1.88.1", features = ["rustls"] }
Basic TLS Configuration
use salvo::prelude::*;
use salvo::conn::rustls::{Keycert, RustlsConfig};
#[handler]
async fn hello() -> &'static str {
"Hello over HTTPS!"
}
#[tokio::main]
async fn main() {
let router = Router::new().get(hello);
// Load certificate and private key
let config = RustlsConfig::new(
Keycert::new()
.cert_from_path("certs/cert.pem")
.unwrap()
.key_from_path("certs/key.pem")
.unwrap()
);
let acceptor = TcpListener::new("0.0.0.0:443")
.rustls(config)
.bind()
.await;
Server::new(acceptor).serve(router).await;
}
Certificate from Memory
use salvo::conn::rustls::{Keycert, RustlsConfig};
let cert_pem = include_bytes!("../certs/cert.pem");
let key_pem = include_bytes!("../certs/key.pem");
let config = RustlsConfig::new(
Keycert::new()
.cert(cert_pem.to_vec())
.key(key_pem.to_vec())
);
ACME (Let's Encrypt) Auto-Certificates
Setup
[dependencies]
salvo = { version = "1.88.1", features = ["acme"] }
HTTP-01 Challenge
use salvo::prelude::*;
use salvo::conn::acme::{AcmeConfig, AcmeListener, ChallengeType};
#[handler]
async fn hello() -> &'static str {
"Hello with auto-certificate!"
}
#[tokio::main]
async fn main() {
let router = Router::new().get(hello);
// Configure ACME
let config = AcmeConfig::builder()
.domains(["example.com", "www.example.com"])
.contacts(["mailto:[email protected]"])
.challenge_type(ChallengeType::Http01)
.cache_path("./acme_cache")
.build()
.unwrap();
// ACME listener handles HTTP-01 challenges and serves HTTPS
let acceptor = AcmeListener::builder()
.acme_config(config)
.bind("0.0.0.0:443")
.await;
Server::new(acceptor).serve(router).await;
}
TLS-ALPN-01 Challenge
For environments where port 80 is not available:
use salvo::conn::acme::{AcmeConfig, AcmeListener, ChallengeType};
let config = AcmeConfig::builder()
.domains(["example.com"])
.contacts(["mailto:[email protected]"])
.challenge_type(ChallengeType::TlsAlpn01)
.cache_path("./acme_cache")
.build()
.unwrap();
Force HTTPS Redirect
use salvo::prelude::*;
#[handler]
async fn force_https(req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
// Check if request is HTTP (not HTTPS)
if req.uri().scheme_str() == Some("http") {
let host = req.header::<String>("Host").unwrap_or_default();
let path = req.uri().path_and_query().map(|p| p.as_str()).unwrap_or("/");
let https_url = format!("https://{}{}", host, path);
res.status_code(StatusCode::MOVED_PERMANENTLY);
res.headers_mut().insert("Location", https_url.parse().unwrap());
ctrl.skip_rest();
return;
}
ctrl.call_next(req, depot, res).await;
}
HTTP and HTTPS on Different Ports
use salvo::prelude::*;
use salvo::conn::rustls::{Keycert, RustlsConfig};
#[tokio::main]
async fn main() {
let router = Router::new().get(hello);
// HTTPS on 443
let tls_config = RustlsConfig::new(
Keycert::new()
.cert_from_path("certs/cert.pem").unwrap()
.key_from_path("certs/key.pem").unwrap()
);
let https_acceptor = TcpListener::new("0.0.0.0:443")
.rustls(tls_config)
.bind()
.await;
// HTTP on 80 (for redirects or ACME challenges)
let http_acceptor = TcpListener::new("0.0.0.0:80")
.bind()
.await;
// Run both servers
tokio::join!(
Server::new(https_acceptor).serve(router.clone()),
Server::new(http_acceptor).serve(Router::new().hoop(redirect_to_https)),
);
}
#[handler]
async fn redirect_to_https(req: &mut Request, res: &mut Response) {
let host = req.header::<String>("Host").unwrap_or_default();
let path = req.uri().path();
res.render(salvo::writing::Redirect::permanent(format!("https://{}{}", host, path)));
}
Certificate Hot Reload
use salvo::prelude::*;
use salvo::conn::rustls::{Keycert, RustlsConfig};
use std::sync::Arc;
use tokio::sync::RwLock;
// Reload certificates without restarting
async fn reload_certificates(config: Arc<RwLock<RustlsConfig>>) {
let new_config = RustlsConfig::new(
Keycert::new()
.cert_from_path("certs/cert.pem").unwrap()
.key_from_path("certs/key.pem").unwrap()
);
let mut guard = config.write().await;
*guard = new_config;
}
HTTP/2 Support
HTTP/2 is automatically enabled when using Rustls:
use salvo::prelude::*;
use salvo::conn::rustls::{Keycert, RustlsConfig};
// HTTP/2 is enabled by default with TLS
let config = RustlsConfig::new(
Keycert::new()
.cert_from_path("certs/cert.pem").unwrap()
.key_from_path("certs/key.pem").unwrap()
);
HTTP/3 (QUIC) Support
[dependencies]
salvo = { version = "1.88.1", features = ["quinn"] }
use salvo::prelude::*;
use salvo::conn::quinn::QuinnListener;
#[tokio::main]
async fn main() {
let router = Router::new().get(hello);
let acceptor = QuinnListener::builder()
.cert_path("certs/cert.pem")
.key_path("certs/key.pem")
.bind("0.0.0.0:443")
.await;
Server::new(acceptor).serve(router).await;
}
Security Headers for HTTPS
use salvo::prelude::*;
#[handler]
async fn security_headers(req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
// HTTP Strict Transport Security
res.headers_mut().insert(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload".parse().unwrap()
);
// Prevent mixed content
res.headers_mut().insert(
"Content-Security-Policy",
"upgrade-insecure-requests".parse().unwrap()
);
ctrl.call_next(req, depot, res).await;
}
Complete ACME Example
use salvo::prelude::*;
use salvo::conn::acme::{AcmeConfig, AcmeListener, ChallengeType};
#[handler]
async fn hello() -> &'static str {
"Hello with Let's Encrypt!"
}
#[handler]
async fn security_headers(req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
res.headers_mut().insert(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains".parse().unwrap()
);
ctrl.call_next(req, depot, res).await;
}
#[tokio::main]
async fn main() {
let router = Router::new()
.hoop(security_headers)
.get(hello);
let acme_config = AcmeConfig::builder()
.domains(["example.com", "www.example.com"])
.contacts(["mailto:[email protected]"])
.challenge_type(ChallengeType::Http01)
.cache_path("./acme_cache")
.directory_url("https://acme-v02.api.letsencrypt.org/directory")
.build()
.unwrap();
let acceptor = AcmeListener::builder()
.acme_config(acme_config)
.bind("0.0.0.0:443")
.await;
println!("Server running on https://example.com");
Server::new(acceptor).serve(router).await;
}
Staging Environment
Use Let's Encrypt staging for testing:
let acme_config = AcmeConfig::builder()
.domains(["example.com"])
.contacts(["mailto:[email protected]"])
.directory_url("https://acme-staging-v02.api.letsencrypt.org/directory") // Staging
.build()
.unwrap();
Best Practices
- Use ACME in production: Automatic certificate renewal
- Set HSTS header: Force browsers to use HTTPS
- Enable HTTP/2: Better performance with TLS
- Test with staging: Use Let's Encrypt staging before production
- Cache certificates: Persist to disk for restart recovery
- Monitor expiration: Alert before certificates expire
- Redirect HTTP to HTTPS: Don't serve content over HTTP
- Use strong ciphers: Let Rustls handle cipher selection
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.