Skip to content

Commit

Permalink
refactor: adjust lets encrypt handle
Browse files Browse the repository at this point in the history
  • Loading branch information
vicanso committed Apr 23, 2024
1 parent f848b59 commit 589e1db
Show file tree
Hide file tree
Showing 13 changed files with 225 additions and 36 deletions.
5 changes: 4 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
- [ ] tls cert auto update[instant-acme]
- [ ] support validate config before save(web)
- [ ] http response cache
- [ ] headers for location support get from env
- [ ] send more event to webhook
- [ ] delay restart
- [ ] redirect http to https(orginal uri)
- [x] headers for location support get from env
- [x] basic auth
- [x] allow none upstream for location
- [x] allow deny ip proxy plugin
Expand Down
3 changes: 3 additions & 0 deletions conf/pingap.toml
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,6 @@ locations = ["lo"]
# tls cert and key, it should be base64
# tls_cert = ""
# tls_key = ""

# Acme
# lets_encrypt = "domain1,domain2"
77 changes: 56 additions & 21 deletions src/acme/lets_encrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// limitations under the License.

use crate::http_extra::HttpResponse;
use crate::state::State;
use crate::state::{restart, State};
use crate::util;

use super::{CertInfo, Error, Result};
Expand All @@ -30,11 +30,13 @@ use pingora::proxy::Session;
use pingora::{server::ShutdownWatch, services::background::BackgroundService};
use rcgen::{Certificate, CertificateParams, DistinguishedName};
use std::collections::HashMap;
use std::env;
use std::path::PathBuf;
use std::time::Duration;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::sync::Mutex;
use tokio::time::interval;

static LETS_ENCRYPT: OnceCell<Mutex<HashMap<String, String>>> = OnceCell::new();

Expand All @@ -48,34 +50,56 @@ pub struct LetsEncryptService {

#[async_trait]
impl BackgroundService for LetsEncryptService {
async fn start(&self, mut _shutdown: ShutdownWatch) {
if let Err(e) = lets_encrypt(&self.domains).await {
error!("lets encrypt, {e}");
async fn start(&self, mut shutdown: ShutdownWatch) {
let mut domains = self.domains.clone();
domains.sort();

let mut period = interval(Duration::from_secs(5 * 60));
loop {
tokio::select! {
_ = shutdown.changed() => {
break;
}
_ = period.tick() => {
let should_fresh_now = if let Ok(cert) = get_lets_encrypt_cert() {
!cert.valid() || domains.join(",") != cert.domains.join(",")
} else {
true
};
if should_fresh_now {
match new_lets_encrypt(&domains).await {
Ok(()) => {
if let Err(e) = restart() {
error!("Restart fail: {e}");
}
},
Err(e) => error!("{e}"),
};
}
}
}
}
}
}

fn get_lets_encrypt_cert_file(domains: &[String]) -> Result<PathBuf> {
let tmp = dirs::template_dir().ok_or(Error::Fail {
message: "get temp dir fail".to_string(),
})?;
Ok(tmp.join(format!("{}.json", domains.join("_"))))
fn get_lets_encrypt_cert_file() -> PathBuf {
env::temp_dir().join("pingap-lets-encrypt.json")
}

pub async fn get_lets_encrypt_cert(domains: &[String]) -> Result<CertInfo> {
let path = get_lets_encrypt_cert_file(domains)?;
pub fn get_lets_encrypt_cert() -> Result<CertInfo> {
let path = get_lets_encrypt_cert_file();
if !path.exists() {
return Err(Error::NotFound {
message: "cert file not found".to_string(),
});
}
let buf = fs::read(&path).await.map_err(|e| Error::Io { source: e })?;
let buf = std::fs::read(&path).map_err(|e| Error::Io { source: e })?;
let cert: CertInfo =
serde_json::from_slice(&buf).map_err(|e| Error::SerdeJson { source: e })?;
Ok(cert)
}

pub async fn handle_lets_encrypt(session: &mut Session, _ctx: &mut State) -> pingora::Result<bool> {
pub async fn handle_lets_encrypt(session: &mut Session, ctx: &mut State) -> pingora::Result<bool> {
let path = session.req_header().uri.path();
if path.starts_with("/.well-known/acme-challenge/") {
let value = {
Expand All @@ -85,28 +109,34 @@ pub async fn handle_lets_encrypt(session: &mut Session, _ctx: &mut State) -> pin
.ok_or_else(|| util::new_internal_error(400, "token not found".to_string()))?;
v.clone()
};
HttpResponse {
let size = HttpResponse {
status: StatusCode::OK,
body: Bytes::from(value),
..Default::default()
}
.send(session)
.await?;
ctx.response_body_size = size;
return Ok(true);
}
Ok(false)
}

async fn lets_encrypt(domains: &[String]) -> Result<()> {
let path = get_lets_encrypt_cert_file(domains)?;

async fn new_lets_encrypt(domains: &[String]) -> Result<()> {
let mut domains: Vec<String> = domains.to_vec();
domains.sort();
let path = get_lets_encrypt_cert_file();
info!(
"lets encrypt start generate acme, domains:{}",
domains.join(",")
);
let (account, _) = Account::create(
&NewAccount {
contact: &[],
terms_of_service_agreed: true,
only_return_existing: false,
},
LetsEncrypt::Staging.url(),
LetsEncrypt::Production.url(),
None,
)
.await
Expand All @@ -125,7 +155,9 @@ async fn lets_encrypt(domains: &[String]) -> Result<()> {

let state = order.state();
if !matches!(state.status, OrderStatus::Pending) {
// TODO return err
return Err(Error::Fail {
message: format!("state status is not pending, {:?}", state.status),
});
}

let authorizations = order
Expand All @@ -135,6 +167,7 @@ async fn lets_encrypt(domains: &[String]) -> Result<()> {
let mut challenges = Vec::with_capacity(authorizations.len());

for authz in &authorizations {
info!("lets encrypt authz status:{:?}", authz.status);
match authz.status {
instant_acme::AuthorizationStatus::Pending => {}
instant_acme::AuthorizationStatus::Valid => continue,
Expand All @@ -154,10 +187,11 @@ async fn lets_encrypt(domains: &[String]) -> Result<()> {
let key_auth = order.key_authorization(challenge);

// http://<你的域名>/.well-known/acme-challenge/<TOKEN>
let well_nkown_path = format!("/.well-known/acme-challenge/{}", challenge.token);
let well_known_path = format!("/.well-known/acme-challenge/{}", challenge.token);
info!("lets encrypt well known path: {well_known_path}");

let mut map = get_lets_encrypt().lock().await;
map.insert(well_nkown_path, key_auth.as_str().to_string());
map.insert(well_known_path, key_auth.as_str().to_string());

challenges.push((identifier, &challenge.url));
}
Expand Down Expand Up @@ -240,6 +274,7 @@ async fn lets_encrypt(domains: &[String]) -> Result<()> {
.await
.map_err(|e| Error::Io { source: e })?;
let info = CertInfo {
domains: domains.to_vec(),
not_after,
not_before,
pem: STANDARD.encode(cert_chain_pem.as_bytes()),
Expand Down
23 changes: 22 additions & 1 deletion src/acme/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use base64::{engine::general_purpose::STANDARD, Engine};
use serde::{Deserialize, Serialize};
use snafu::Snafu;
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Debug, Snafu)]
pub enum Error {
Expand All @@ -35,12 +37,31 @@ type Result<T, E = Error> = std::result::Result<T, E>;

#[derive(Debug, Deserialize, Serialize)]
pub struct CertInfo {
pub domains: Vec<String>,
pub not_after: i64,
pub not_before: i64,
pub pem: String,
pub key: String,
}
impl CertInfo {
pub fn valid(&self) -> bool {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
if self.not_before > ts {
return false;
}
self.not_after - ts > 3600
}
pub fn get_cert(&self) -> Vec<u8> {
STANDARD.decode(&self.pem).unwrap_or_default()
}
pub fn get_key(&self) -> Vec<u8> {
STANDARD.decode(&self.key).unwrap_or_default()
}
}

mod lets_encrypt;

pub use lets_encrypt::{handle_lets_encrypt, LetsEncryptService};
pub use lets_encrypt::{get_lets_encrypt_cert, handle_lets_encrypt, LetsEncryptService};
1 change: 1 addition & 0 deletions src/config/load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pub enum ProxyPluginCategory {
KeyAuth,
BasicAuth,
Cache,
RedirectHttps,
}

#[derive(PartialEq, Debug, Default, Deserialize_repr, Clone, Copy, Serialize_repr)]
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ fn run() -> Result<(), Box<dyn Error>> {
},
));
server_conf_list.push(ServerConf {
name: "admin".to_string(),
name: "pingap:admin".to_string(),
admin: true,
addr,
..Default::default()
Expand Down
5 changes: 5 additions & 0 deletions src/plugin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ mod ip_limit;
mod key_auth;
mod limit;
mod mock;
mod redirect_https;
mod request_id;
mod stats;

Expand Down Expand Up @@ -143,6 +144,10 @@ pub fn init_proxy_plugins(confs: Vec<(String, ProxyPluginConf)>) -> Result<()> {
let c = cache::Cache::new(&conf.value, step)?;
plguins.insert(name, Box::new(c));
}
ProxyPluginCategory::RedirectHttps => {
let r = redirect_https::RedirectHttps::new(&conf.value, step)?;
plguins.insert(name, Box::new(r));
}
};
}

Expand Down
75 changes: 75 additions & 0 deletions src/plugin/redirect_https.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2024 Tree xie.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use super::ProxyPlugin;
use super::Result;
use crate::config::ProxyPluginCategory;
use crate::config::ProxyPluginStep;
use crate::http_extra::convert_headers;
use crate::http_extra::HttpResponse;
use crate::state::State;
use async_trait::async_trait;
use http::StatusCode;
use pingora::proxy::Session;

pub struct RedirectHttps {
prefix: String,
proxy_step: ProxyPluginStep,
}

impl RedirectHttps {
pub fn new(value: &str, proxy_step: ProxyPluginStep) -> Result<Self> {
let mut prefix = "".to_string();
if value.trim().len() > 1 {
prefix = value.trim().to_string();
}
Ok(Self { prefix, proxy_step })
}
}

#[async_trait]
impl ProxyPlugin for RedirectHttps {
#[inline]
fn step(&self) -> ProxyPluginStep {
self.proxy_step
}
#[inline]
fn category(&self) -> ProxyPluginCategory {
ProxyPluginCategory::RedirectHttps
}
#[inline]
async fn handle(&self, session: &mut Session, ctx: &mut State) -> pingora::Result<bool> {
if !ctx.is_tls {
let host = if let Some(value) = session.get_header("Host") {
value.to_str().unwrap_or_default()
} else {
session.req_header().uri.host().unwrap_or_default()
};
let location = format!(
"Location: https://{host}{}{}",
self.prefix,
session.req_header().uri
);
let _ = HttpResponse {
status: StatusCode::TEMPORARY_REDIRECT,
headers: Some(convert_headers(&[location]).unwrap_or_default()),
..Default::default()
}
.send(session)
.await?;
return Ok(true);
}
Ok(false)
}
}
6 changes: 5 additions & 1 deletion src/proxy/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,11 @@ impl Parser {
}
}
TagCategory::Scheme => {
// TODO
if ctx.is_tls {
buf.extend(b"https");
} else {
buf.extend(b"http");
}
}
TagCategory::Uri => {
buf.extend(req_header.uri.to_string().as_bytes());
Expand Down
Loading

0 comments on commit 589e1db

Please sign in to comment.