wip: refactor whole module in lib
This commit is contained in:
parent
7bc6e30935
commit
f98c778a0c
42 changed files with 943 additions and 531 deletions
135
legacy-lib/src/backend/load_balance.rs
Normal file
135
legacy-lib/src/backend/load_balance.rs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
#[cfg(feature = "sticky-cookie")]
|
||||
pub use super::{
|
||||
load_balance_sticky::{LbStickyRoundRobin, LbStickyRoundRobinBuilder},
|
||||
sticky_cookie::StickyCookie,
|
||||
};
|
||||
use derive_builder::Builder;
|
||||
use rand::Rng;
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
/// Constants to specify a load balance option
|
||||
pub(super) mod load_balance_options {
|
||||
pub const FIX_TO_FIRST: &str = "none";
|
||||
pub const ROUND_ROBIN: &str = "round_robin";
|
||||
pub const RANDOM: &str = "random";
|
||||
#[cfg(feature = "sticky-cookie")]
|
||||
pub const STICKY_ROUND_ROBIN: &str = "sticky";
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Pointer to upstream serving the incoming request.
|
||||
/// If 'sticky cookie'-based LB is enabled and cookie must be updated/created, the new cookie is also given.
|
||||
pub(super) struct PointerToUpstream {
|
||||
pub ptr: usize,
|
||||
pub context_lb: Option<LbContext>,
|
||||
}
|
||||
/// Trait for LB
|
||||
pub(super) trait LbWithPointer {
|
||||
fn get_ptr(&self, req_info: Option<&LbContext>) -> PointerToUpstream;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Builder)]
|
||||
/// Round Robin LB object as a pointer to the current serving upstream destination
|
||||
pub struct LbRoundRobin {
|
||||
#[builder(default)]
|
||||
/// Pointer to the index of the last served upstream destination
|
||||
ptr: Arc<AtomicUsize>,
|
||||
#[builder(setter(custom), default)]
|
||||
/// Number of upstream destinations
|
||||
num_upstreams: usize,
|
||||
}
|
||||
impl LbRoundRobinBuilder {
|
||||
pub fn num_upstreams(&mut self, v: &usize) -> &mut Self {
|
||||
self.num_upstreams = Some(*v);
|
||||
self
|
||||
}
|
||||
}
|
||||
impl LbWithPointer for LbRoundRobin {
|
||||
/// Increment the count of upstream served up to the max value
|
||||
fn get_ptr(&self, _info: Option<&LbContext>) -> PointerToUpstream {
|
||||
// Get a current count of upstream served
|
||||
let current_ptr = self.ptr.load(Ordering::Relaxed);
|
||||
|
||||
let ptr = if current_ptr < self.num_upstreams - 1 {
|
||||
self.ptr.fetch_add(1, Ordering::Relaxed)
|
||||
} else {
|
||||
// Clear the counter
|
||||
self.ptr.fetch_and(0, Ordering::Relaxed)
|
||||
};
|
||||
PointerToUpstream { ptr, context_lb: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Builder)]
|
||||
/// Random LB object to keep the object of random pools
|
||||
pub struct LbRandom {
|
||||
#[builder(setter(custom), default)]
|
||||
/// Number of upstream destinations
|
||||
num_upstreams: usize,
|
||||
}
|
||||
impl LbRandomBuilder {
|
||||
pub fn num_upstreams(&mut self, v: &usize) -> &mut Self {
|
||||
self.num_upstreams = Some(*v);
|
||||
self
|
||||
}
|
||||
}
|
||||
impl LbWithPointer for LbRandom {
|
||||
/// Returns the random index within the range
|
||||
fn get_ptr(&self, _info: Option<&LbContext>) -> PointerToUpstream {
|
||||
let mut rng = rand::thread_rng();
|
||||
let ptr = rng.gen_range(0..self.num_upstreams);
|
||||
PointerToUpstream { ptr, context_lb: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Load Balancing Option
|
||||
pub enum LoadBalance {
|
||||
/// Fix to the first upstream. Use if only one upstream destination is specified
|
||||
FixToFirst,
|
||||
/// Randomly chose one upstream server
|
||||
Random(LbRandom),
|
||||
/// Simple round robin without session persistance
|
||||
RoundRobin(LbRoundRobin),
|
||||
#[cfg(feature = "sticky-cookie")]
|
||||
/// Round robin with session persistance using cookie
|
||||
StickyRoundRobin(LbStickyRoundRobin),
|
||||
}
|
||||
impl Default for LoadBalance {
|
||||
fn default() -> Self {
|
||||
Self::FixToFirst
|
||||
}
|
||||
}
|
||||
|
||||
impl LoadBalance {
|
||||
/// Get the index of the upstream serving the incoming request
|
||||
pub(super) fn get_context(&self, _context_to_lb: &Option<LbContext>) -> PointerToUpstream {
|
||||
match self {
|
||||
LoadBalance::FixToFirst => PointerToUpstream {
|
||||
ptr: 0usize,
|
||||
context_lb: None,
|
||||
},
|
||||
LoadBalance::RoundRobin(ptr) => ptr.get_ptr(None),
|
||||
LoadBalance::Random(ptr) => ptr.get_ptr(None),
|
||||
#[cfg(feature = "sticky-cookie")]
|
||||
LoadBalance::StickyRoundRobin(ptr) => {
|
||||
// Generate new context if sticky round robin is enabled.
|
||||
ptr.get_ptr(_context_to_lb.as_ref())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Struct to handle the sticky cookie string,
|
||||
/// - passed from Rp module (http handler) to LB module, manipulated from req, only StickyCookieValue exists.
|
||||
/// - passed from LB module to Rp module (http handler), will be inserted into res, StickyCookieValue and Info exist.
|
||||
pub struct LbContext {
|
||||
#[cfg(feature = "sticky-cookie")]
|
||||
pub sticky_cookie: StickyCookie,
|
||||
#[cfg(not(feature = "sticky-cookie"))]
|
||||
pub sticky_cookie: (),
|
||||
}
|
||||
132
legacy-lib/src/backend/load_balance_sticky.rs
Normal file
132
legacy-lib/src/backend/load_balance_sticky.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
use super::{
|
||||
load_balance::{LbContext, LbWithPointer, PointerToUpstream},
|
||||
sticky_cookie::StickyCookieConfig,
|
||||
Upstream,
|
||||
};
|
||||
use crate::{constants::STICKY_COOKIE_NAME, log::*};
|
||||
use derive_builder::Builder;
|
||||
use rustc_hash::FxHashMap as HashMap;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Builder)]
|
||||
/// Round Robin LB object in the sticky cookie manner
|
||||
pub struct LbStickyRoundRobin {
|
||||
#[builder(default)]
|
||||
/// Pointer to the index of the last served upstream destination
|
||||
ptr: Arc<AtomicUsize>,
|
||||
#[builder(setter(custom), default)]
|
||||
/// Number of upstream destinations
|
||||
num_upstreams: usize,
|
||||
#[builder(setter(custom))]
|
||||
/// Information to build the cookie to stick clients to specific backends
|
||||
pub sticky_config: StickyCookieConfig,
|
||||
#[builder(setter(custom))]
|
||||
/// Hashmaps:
|
||||
/// - Hashmap that maps server indices to server id (string)
|
||||
/// - Hashmap that maps server ids (string) to server indices, for fast reverse lookup
|
||||
upstream_maps: UpstreamMap,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UpstreamMap {
|
||||
/// Hashmap that maps server indices to server id (string)
|
||||
upstream_index_map: Vec<String>,
|
||||
/// Hashmap that maps server ids (string) to server indices, for fast reverse lookup
|
||||
upstream_id_map: HashMap<String, usize>,
|
||||
}
|
||||
impl LbStickyRoundRobinBuilder {
|
||||
pub fn num_upstreams(&mut self, v: &usize) -> &mut Self {
|
||||
self.num_upstreams = Some(*v);
|
||||
self
|
||||
}
|
||||
pub fn sticky_config(&mut self, server_name: &str, path_opt: &Option<String>) -> &mut Self {
|
||||
self.sticky_config = Some(StickyCookieConfig {
|
||||
name: STICKY_COOKIE_NAME.to_string(), // TODO: config等で変更できるように
|
||||
domain: server_name.to_ascii_lowercase(),
|
||||
path: if let Some(v) = path_opt {
|
||||
v.to_ascii_lowercase()
|
||||
} else {
|
||||
"/".to_string()
|
||||
},
|
||||
duration: 300, // TODO: config等で変更できるように
|
||||
});
|
||||
self
|
||||
}
|
||||
pub fn upstream_maps(&mut self, upstream_vec: &[Upstream]) -> &mut Self {
|
||||
let upstream_index_map: Vec<String> = upstream_vec
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| v.calculate_id_with_index(i))
|
||||
.collect();
|
||||
let mut upstream_id_map = HashMap::default();
|
||||
for (i, v) in upstream_index_map.iter().enumerate() {
|
||||
upstream_id_map.insert(v.to_string(), i);
|
||||
}
|
||||
self.upstream_maps = Some(UpstreamMap {
|
||||
upstream_index_map,
|
||||
upstream_id_map,
|
||||
});
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<'a> LbStickyRoundRobin {
|
||||
fn simple_increment_ptr(&self) -> usize {
|
||||
// Get a current count of upstream served
|
||||
let current_ptr = self.ptr.load(Ordering::Relaxed);
|
||||
|
||||
if current_ptr < self.num_upstreams - 1 {
|
||||
self.ptr.fetch_add(1, Ordering::Relaxed)
|
||||
} else {
|
||||
// Clear the counter
|
||||
self.ptr.fetch_and(0, Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
/// This is always called only internally. So 'unwrap()' is executed.
|
||||
fn get_server_id_from_index(&self, index: usize) -> String {
|
||||
self.upstream_maps.upstream_index_map.get(index).unwrap().to_owned()
|
||||
}
|
||||
/// This function takes value passed from outside. So 'result' is used.
|
||||
fn get_server_index_from_id(&self, id: impl Into<Cow<'a, str>>) -> Option<usize> {
|
||||
let id_str = id.into().to_string();
|
||||
self.upstream_maps.upstream_id_map.get(&id_str).map(|v| v.to_owned())
|
||||
}
|
||||
}
|
||||
impl LbWithPointer for LbStickyRoundRobin {
|
||||
fn get_ptr(&self, req_info: Option<&LbContext>) -> PointerToUpstream {
|
||||
// If given context is None or invalid (not contained), get_ptr() is invoked to increment the pointer.
|
||||
// Otherwise, get the server index indicated by the server_id inside the cookie
|
||||
let ptr = match req_info {
|
||||
None => {
|
||||
debug!("No sticky cookie");
|
||||
self.simple_increment_ptr()
|
||||
}
|
||||
Some(context) => {
|
||||
let server_id = &context.sticky_cookie.value.value;
|
||||
if let Some(server_index) = self.get_server_index_from_id(server_id) {
|
||||
debug!("Valid sticky cookie: id={}, index={}", server_id, server_index);
|
||||
server_index
|
||||
} else {
|
||||
debug!("Invalid sticky cookie: id={}", server_id);
|
||||
self.simple_increment_ptr()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Get the server id from the ptr.
|
||||
// TODO: This should be simplified and optimized if ptr is not changed (id value exists in cookie).
|
||||
let upstream_id = self.get_server_id_from_index(ptr);
|
||||
let new_cookie = self.sticky_config.build_sticky_cookie(upstream_id).unwrap();
|
||||
let new_context = Some(LbContext {
|
||||
sticky_cookie: new_cookie,
|
||||
});
|
||||
PointerToUpstream {
|
||||
ptr,
|
||||
context_lb: new_context,
|
||||
}
|
||||
}
|
||||
}
|
||||
77
legacy-lib/src/backend/mod.rs
Normal file
77
legacy-lib/src/backend/mod.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
mod load_balance;
|
||||
#[cfg(feature = "sticky-cookie")]
|
||||
mod load_balance_sticky;
|
||||
#[cfg(feature = "sticky-cookie")]
|
||||
mod sticky_cookie;
|
||||
mod upstream;
|
||||
mod upstream_opts;
|
||||
|
||||
#[cfg(feature = "sticky-cookie")]
|
||||
pub use self::sticky_cookie::{StickyCookie, StickyCookieValue};
|
||||
pub use self::{
|
||||
load_balance::{LbContext, LoadBalance},
|
||||
upstream::{ReverseProxy, Upstream, UpstreamGroup, UpstreamGroupBuilder},
|
||||
upstream_opts::UpstreamOption,
|
||||
};
|
||||
use crate::{
|
||||
certs::CryptoSource,
|
||||
utils::{BytesName, PathNameBytesExp, ServerNameBytesExp},
|
||||
};
|
||||
use derive_builder::Builder;
|
||||
use rustc_hash::FxHashMap as HashMap;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Struct serving information to route incoming connections, like server name to be handled and tls certs/keys settings.
|
||||
#[derive(Builder)]
|
||||
pub struct Backend<T>
|
||||
where
|
||||
T: CryptoSource,
|
||||
{
|
||||
#[builder(setter(into))]
|
||||
/// backend application name, e.g., app1
|
||||
pub app_name: String,
|
||||
#[builder(setter(custom))]
|
||||
/// server name, e.g., example.com, in String ascii lower case
|
||||
pub server_name: String,
|
||||
/// struct of reverse proxy serving incoming request
|
||||
pub reverse_proxy: ReverseProxy,
|
||||
|
||||
/// tls settings: https redirection with 30x
|
||||
#[builder(default)]
|
||||
pub https_redirection: Option<bool>,
|
||||
|
||||
/// TLS settings: source meta for server cert, key, client ca cert
|
||||
#[builder(default)]
|
||||
pub crypto_source: Option<T>,
|
||||
}
|
||||
impl<'a, T> BackendBuilder<T>
|
||||
where
|
||||
T: CryptoSource,
|
||||
{
|
||||
pub fn server_name(&mut self, server_name: impl Into<Cow<'a, str>>) -> &mut Self {
|
||||
self.server_name = Some(server_name.into().to_ascii_lowercase());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// HashMap and some meta information for multiple Backend structs.
|
||||
pub struct Backends<T>
|
||||
where
|
||||
T: CryptoSource,
|
||||
{
|
||||
pub apps: HashMap<ServerNameBytesExp, Backend<T>>, // hyper::uriで抜いたhostで引っ掛ける
|
||||
pub default_server_name_bytes: Option<ServerNameBytesExp>, // for plaintext http
|
||||
}
|
||||
|
||||
impl<T> Backends<T>
|
||||
where
|
||||
T: CryptoSource,
|
||||
{
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
Backends {
|
||||
apps: HashMap::<ServerNameBytesExp, Backend<T>>::default(),
|
||||
default_server_name_bytes: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
208
legacy-lib/src/backend/sticky_cookie.rs
Normal file
208
legacy-lib/src/backend/sticky_cookie.rs
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use crate::error::*;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use derive_builder::Builder;
|
||||
|
||||
#[derive(Debug, Clone, Builder)]
|
||||
/// Cookie value only, used for COOKIE in req
|
||||
pub struct StickyCookieValue {
|
||||
#[builder(setter(custom))]
|
||||
/// Field name indicating sticky cookie
|
||||
pub name: String,
|
||||
#[builder(setter(custom))]
|
||||
/// Upstream server_id
|
||||
pub value: String,
|
||||
}
|
||||
impl<'a> StickyCookieValueBuilder {
|
||||
pub fn name(&mut self, v: impl Into<Cow<'a, str>>) -> &mut Self {
|
||||
self.name = Some(v.into().to_ascii_lowercase());
|
||||
self
|
||||
}
|
||||
pub fn value(&mut self, v: impl Into<Cow<'a, str>>) -> &mut Self {
|
||||
self.value = Some(v.into().to_string());
|
||||
self
|
||||
}
|
||||
}
|
||||
impl StickyCookieValue {
|
||||
pub fn try_from(value: &str, expected_name: &str) -> Result<Self> {
|
||||
if !value.starts_with(expected_name) {
|
||||
return Err(RpxyError::LoadBalance(
|
||||
"Failed to cookie conversion from string".to_string(),
|
||||
));
|
||||
};
|
||||
let kv = value.split('=').map(|v| v.trim()).collect::<Vec<&str>>();
|
||||
if kv.len() != 2 {
|
||||
return Err(RpxyError::LoadBalance("Invalid cookie structure".to_string()));
|
||||
};
|
||||
if kv[1].is_empty() {
|
||||
return Err(RpxyError::LoadBalance("No sticky cookie value".to_string()));
|
||||
}
|
||||
Ok(StickyCookieValue {
|
||||
name: expected_name.to_string(),
|
||||
value: kv[1].to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Builder)]
|
||||
/// Struct describing sticky cookie meta information used for SET-COOKIE in res
|
||||
pub struct StickyCookieInfo {
|
||||
#[builder(setter(custom))]
|
||||
/// Unix time
|
||||
pub expires: i64,
|
||||
|
||||
#[builder(setter(custom))]
|
||||
/// Domain
|
||||
pub domain: String,
|
||||
|
||||
#[builder(setter(custom))]
|
||||
/// Path
|
||||
pub path: String,
|
||||
}
|
||||
impl<'a> StickyCookieInfoBuilder {
|
||||
pub fn domain(&mut self, v: impl Into<Cow<'a, str>>) -> &mut Self {
|
||||
self.domain = Some(v.into().to_ascii_lowercase());
|
||||
self
|
||||
}
|
||||
pub fn path(&mut self, v: impl Into<Cow<'a, str>>) -> &mut Self {
|
||||
self.path = Some(v.into().to_ascii_lowercase());
|
||||
self
|
||||
}
|
||||
pub fn expires(&mut self, duration_secs: i64) -> &mut Self {
|
||||
let current = Utc::now().timestamp();
|
||||
self.expires = Some(current + duration_secs);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Builder)]
|
||||
/// Struct describing sticky cookie
|
||||
pub struct StickyCookie {
|
||||
#[builder(setter(custom))]
|
||||
/// Upstream server_id
|
||||
pub value: StickyCookieValue,
|
||||
#[builder(setter(custom), default)]
|
||||
/// Upstream server_id
|
||||
pub info: Option<StickyCookieInfo>,
|
||||
}
|
||||
|
||||
impl<'a> StickyCookieBuilder {
|
||||
pub fn value(&mut self, n: impl Into<Cow<'a, str>>, v: impl Into<Cow<'a, str>>) -> &mut Self {
|
||||
self.value = Some(StickyCookieValueBuilder::default().name(n).value(v).build().unwrap());
|
||||
self
|
||||
}
|
||||
pub fn info(
|
||||
&mut self,
|
||||
domain: impl Into<Cow<'a, str>>,
|
||||
path: impl Into<Cow<'a, str>>,
|
||||
duration_secs: i64,
|
||||
) -> &mut Self {
|
||||
let info = StickyCookieInfoBuilder::default()
|
||||
.domain(domain)
|
||||
.path(path)
|
||||
.expires(duration_secs)
|
||||
.build()
|
||||
.unwrap();
|
||||
self.info = Some(Some(info));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<String> for StickyCookie {
|
||||
type Error = RpxyError;
|
||||
|
||||
fn try_into(self) -> Result<String> {
|
||||
if self.info.is_none() {
|
||||
return Err(RpxyError::LoadBalance(
|
||||
"Failed to cookie conversion into string: no meta information".to_string(),
|
||||
));
|
||||
}
|
||||
let info = self.info.unwrap();
|
||||
let chrono::LocalResult::Single(expires_timestamp) = Utc.timestamp_opt(info.expires, 0) else {
|
||||
return Err(RpxyError::LoadBalance("Failed to cookie conversion into string".to_string()));
|
||||
};
|
||||
let exp_str = expires_timestamp.format("%a, %d-%b-%Y %T GMT").to_string();
|
||||
let max_age = info.expires - Utc::now().timestamp();
|
||||
|
||||
Ok(format!(
|
||||
"{}={}; expires={}; Max-Age={}; path={}; domain={}",
|
||||
self.value.name, self.value.value, exp_str, max_age, info.path, info.domain
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Configuration to serve incoming requests in the manner of "sticky cookie".
|
||||
/// Including a dictionary to map Ids included in cookie and upstream destinations,
|
||||
/// and expiration of cookie.
|
||||
/// "domain" and "path" in the cookie will be the same as the reverse proxy options.
|
||||
pub struct StickyCookieConfig {
|
||||
pub name: String,
|
||||
pub domain: String,
|
||||
pub path: String,
|
||||
pub duration: i64,
|
||||
}
|
||||
impl<'a> StickyCookieConfig {
|
||||
pub fn build_sticky_cookie(&self, v: impl Into<Cow<'a, str>>) -> Result<StickyCookie> {
|
||||
StickyCookieBuilder::default()
|
||||
.value(self.name.clone(), v)
|
||||
.info(&self.domain, &self.path, self.duration)
|
||||
.build()
|
||||
.map_err(|_| RpxyError::LoadBalance("Failed to build sticky cookie from config".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::constants::STICKY_COOKIE_NAME;
|
||||
|
||||
#[test]
|
||||
fn config_works() {
|
||||
let config = StickyCookieConfig {
|
||||
name: STICKY_COOKIE_NAME.to_string(),
|
||||
domain: "example.com".to_string(),
|
||||
path: "/path".to_string(),
|
||||
duration: 100,
|
||||
};
|
||||
let expires_unix = Utc::now().timestamp() + 100;
|
||||
let sc_string: Result<String> = config.build_sticky_cookie("test_value").unwrap().try_into();
|
||||
let expires_date_string = Utc
|
||||
.timestamp_opt(expires_unix, 0)
|
||||
.unwrap()
|
||||
.format("%a, %d-%b-%Y %T GMT")
|
||||
.to_string();
|
||||
assert_eq!(
|
||||
sc_string.unwrap(),
|
||||
format!(
|
||||
"{}=test_value; expires={}; Max-Age={}; path=/path; domain=example.com",
|
||||
STICKY_COOKIE_NAME, expires_date_string, 100
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn to_string_works() {
|
||||
let sc = StickyCookie {
|
||||
value: StickyCookieValue {
|
||||
name: STICKY_COOKIE_NAME.to_string(),
|
||||
value: "test_value".to_string(),
|
||||
},
|
||||
info: Some(StickyCookieInfo {
|
||||
expires: 1686221173i64,
|
||||
domain: "example.com".to_string(),
|
||||
path: "/path".to_string(),
|
||||
}),
|
||||
};
|
||||
let sc_string: Result<String> = sc.try_into();
|
||||
let max_age = 1686221173i64 - Utc::now().timestamp();
|
||||
assert!(sc_string.is_ok());
|
||||
assert_eq!(
|
||||
sc_string.unwrap(),
|
||||
format!(
|
||||
"{}=test_value; expires=Thu, 08-Jun-2023 10:46:13 GMT; Max-Age={}; path=/path; domain=example.com",
|
||||
STICKY_COOKIE_NAME, max_age
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
201
legacy-lib/src/backend/upstream.rs
Normal file
201
legacy-lib/src/backend/upstream.rs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
#[cfg(feature = "sticky-cookie")]
|
||||
use super::load_balance::LbStickyRoundRobinBuilder;
|
||||
use super::load_balance::{load_balance_options as lb_opts, LbRandomBuilder, LbRoundRobinBuilder, LoadBalance};
|
||||
use super::{BytesName, LbContext, PathNameBytesExp, UpstreamOption};
|
||||
use crate::log::*;
|
||||
#[cfg(feature = "sticky-cookie")]
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use derive_builder::Builder;
|
||||
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
|
||||
#[cfg(feature = "sticky-cookie")]
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::borrow::Cow;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReverseProxy {
|
||||
pub upstream: HashMap<PathNameBytesExp, UpstreamGroup>, // TODO: HashMapでいいのかは疑問。max_by_keyでlongest prefix matchしてるのも無駄っぽいが。。。
|
||||
}
|
||||
|
||||
impl ReverseProxy {
|
||||
/// Get an appropriate upstream destination for given path string.
|
||||
pub fn get<'a>(&self, path_str: impl Into<Cow<'a, str>>) -> Option<&UpstreamGroup> {
|
||||
// trie使ってlongest prefix match させてもいいけどルート記述は少ないと思われるので、
|
||||
// コスト的にこの程度で十分
|
||||
let path_bytes = &path_str.to_path_name_vec();
|
||||
|
||||
let matched_upstream = self
|
||||
.upstream
|
||||
.iter()
|
||||
.filter(|(route_bytes, _)| {
|
||||
match path_bytes.starts_with(route_bytes) {
|
||||
true => {
|
||||
route_bytes.len() == 1 // route = '/', i.e., default
|
||||
|| match path_bytes.get(route_bytes.len()) {
|
||||
None => true, // exact case
|
||||
Some(p) => p == &b'/', // sub-path case
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
.max_by_key(|(route_bytes, _)| route_bytes.len());
|
||||
if let Some((_path, u)) = matched_upstream {
|
||||
debug!(
|
||||
"Found upstream: {:?}",
|
||||
String::from_utf8(_path.0.clone()).unwrap_or_else(|_| "<none>".to_string())
|
||||
);
|
||||
Some(u)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Upstream struct just containing uri without path
|
||||
pub struct Upstream {
|
||||
/// Base uri without specific path
|
||||
pub uri: hyper::Uri,
|
||||
}
|
||||
impl Upstream {
|
||||
#[cfg(feature = "sticky-cookie")]
|
||||
/// Hashing uri with index to avoid collision
|
||||
pub fn calculate_id_with_index(&self, index: usize) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
let uri_string = format!("{}&index={}", self.uri.clone(), index);
|
||||
hasher.update(uri_string.as_bytes());
|
||||
let digest = hasher.finalize();
|
||||
general_purpose::URL_SAFE_NO_PAD.encode(digest)
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Builder)]
|
||||
/// Struct serving multiple upstream servers for, e.g., load balancing.
|
||||
pub struct UpstreamGroup {
|
||||
#[builder(setter(custom))]
|
||||
/// Upstream server(s)
|
||||
pub upstream: Vec<Upstream>,
|
||||
#[builder(setter(custom), default)]
|
||||
/// Path like "/path" in [[PathNameBytesExp]] associated with the upstream server(s)
|
||||
pub path: PathNameBytesExp,
|
||||
#[builder(setter(custom), default)]
|
||||
/// Path in [[PathNameBytesExp]] that will be used to replace the "path" part of incoming url
|
||||
pub replace_path: Option<PathNameBytesExp>,
|
||||
|
||||
#[builder(setter(custom), default)]
|
||||
/// Load balancing option
|
||||
pub lb: LoadBalance,
|
||||
#[builder(setter(custom), default)]
|
||||
/// Activated upstream options defined in [[UpstreamOption]]
|
||||
pub opts: HashSet<UpstreamOption>,
|
||||
}
|
||||
|
||||
impl UpstreamGroupBuilder {
|
||||
pub fn upstream(&mut self, upstream_vec: &[Upstream]) -> &mut Self {
|
||||
self.upstream = Some(upstream_vec.to_vec());
|
||||
self
|
||||
}
|
||||
pub fn path(&mut self, v: &Option<String>) -> &mut Self {
|
||||
let path = match v {
|
||||
Some(p) => p.to_path_name_vec(),
|
||||
None => "/".to_path_name_vec(),
|
||||
};
|
||||
self.path = Some(path);
|
||||
self
|
||||
}
|
||||
pub fn replace_path(&mut self, v: &Option<String>) -> &mut Self {
|
||||
self.replace_path = Some(
|
||||
v.to_owned()
|
||||
.as_ref()
|
||||
.map_or_else(|| None, |v| Some(v.to_path_name_vec())),
|
||||
);
|
||||
self
|
||||
}
|
||||
pub fn lb(
|
||||
&mut self,
|
||||
v: &Option<String>,
|
||||
// upstream_num: &usize,
|
||||
upstream_vec: &Vec<Upstream>,
|
||||
_server_name: &str,
|
||||
_path_opt: &Option<String>,
|
||||
) -> &mut Self {
|
||||
let upstream_num = &upstream_vec.len();
|
||||
let lb = if let Some(x) = v {
|
||||
match x.as_str() {
|
||||
lb_opts::FIX_TO_FIRST => LoadBalance::FixToFirst,
|
||||
lb_opts::RANDOM => LoadBalance::Random(LbRandomBuilder::default().num_upstreams(upstream_num).build().unwrap()),
|
||||
lb_opts::ROUND_ROBIN => LoadBalance::RoundRobin(
|
||||
LbRoundRobinBuilder::default()
|
||||
.num_upstreams(upstream_num)
|
||||
.build()
|
||||
.unwrap(),
|
||||
),
|
||||
#[cfg(feature = "sticky-cookie")]
|
||||
lb_opts::STICKY_ROUND_ROBIN => LoadBalance::StickyRoundRobin(
|
||||
LbStickyRoundRobinBuilder::default()
|
||||
.num_upstreams(upstream_num)
|
||||
.sticky_config(_server_name, _path_opt)
|
||||
.upstream_maps(upstream_vec) // TODO:
|
||||
.build()
|
||||
.unwrap(),
|
||||
),
|
||||
_ => {
|
||||
error!("Specified load balancing option is invalid.");
|
||||
LoadBalance::default()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LoadBalance::default()
|
||||
};
|
||||
self.lb = Some(lb);
|
||||
self
|
||||
}
|
||||
pub fn opts(&mut self, v: &Option<Vec<String>>) -> &mut Self {
|
||||
let opts = if let Some(opts) = v {
|
||||
opts
|
||||
.iter()
|
||||
.filter_map(|str| UpstreamOption::try_from(str.as_str()).ok())
|
||||
.collect::<HashSet<UpstreamOption>>()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
self.opts = Some(opts);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl UpstreamGroup {
|
||||
/// Get an enabled option of load balancing [[LoadBalance]]
|
||||
pub fn get(&self, context_to_lb: &Option<LbContext>) -> (Option<&Upstream>, Option<LbContext>) {
|
||||
let pointer_to_upstream = self.lb.get_context(context_to_lb);
|
||||
debug!("Upstream of index {} is chosen.", pointer_to_upstream.ptr);
|
||||
debug!("Context to LB (Cookie in Req): {:?}", context_to_lb);
|
||||
debug!(
|
||||
"Context from LB (Set-Cookie in Res): {:?}",
|
||||
pointer_to_upstream.context_lb
|
||||
);
|
||||
(
|
||||
self.upstream.get(pointer_to_upstream.ptr),
|
||||
pointer_to_upstream.context_lb,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[allow(unused)]
|
||||
use super::*;
|
||||
|
||||
#[cfg(feature = "sticky-cookie")]
|
||||
#[test]
|
||||
fn calc_id_works() {
|
||||
let uri = "https://www.rust-lang.org".parse::<hyper::Uri>().unwrap();
|
||||
let upstream = Upstream { uri };
|
||||
assert_eq!(
|
||||
"eGsjoPbactQ1eUJjafYjPT3ekYZQkaqJnHdA_FMSkgM",
|
||||
upstream.calculate_id_with_index(0)
|
||||
);
|
||||
assert_eq!(
|
||||
"tNVXFJ9eNCT2mFgKbYq35XgH5q93QZtfU8piUiiDxVA",
|
||||
upstream.calculate_id_with_index(1)
|
||||
);
|
||||
}
|
||||
}
|
||||
22
legacy-lib/src/backend/upstream_opts.rs
Normal file
22
legacy-lib/src/backend/upstream_opts.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use crate::error::*;
|
||||
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
||||
pub enum UpstreamOption {
|
||||
OverrideHost,
|
||||
UpgradeInsecureRequests,
|
||||
ForceHttp11Upstream,
|
||||
ForceHttp2Upstream,
|
||||
// TODO: Adds more options for heder override
|
||||
}
|
||||
impl TryFrom<&str> for UpstreamOption {
|
||||
type Error = RpxyError;
|
||||
fn try_from(val: &str) -> Result<Self> {
|
||||
match val {
|
||||
"override_host" => Ok(Self::OverrideHost),
|
||||
"upgrade_insecure_requests" => Ok(Self::UpgradeInsecureRequests),
|
||||
"force_http11_upstream" => Ok(Self::ForceHttp11Upstream),
|
||||
"force_http2_upstream" => Ok(Self::ForceHttp2Upstream),
|
||||
_ => Err(RpxyError::Other(anyhow!("Unsupported header option"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue