首页 > 系统相关 >47从零开始用Rust编写nginx,配对还有这么多要求!负载均衡中的路径匹配

47从零开始用Rust编写nginx,配对还有这么多要求!负载均衡中的路径匹配

时间:2024-02-01 10:46:02浏览次数:28  
标签:return 47 Some Ok nginx let 匹配 false Rust

wmproxy

wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 负载均衡, 静态文件服务器,websocket代理,四层TCP/UDP转发,内网穿透等,会将实现过程分享出来,感兴趣的可以一起造个轮子

项目地址

国内: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

设计目标

负载均衡时通过匹配规则匹配正确的location进行处理相关的操作。

设计方案变更

初始设计方案

初始方案以最快的方式进行支持,仅支持前缀匹配,即如果配置

[[http.server.location]]
rule = "/wmproxy"

那么当我们访问/wmproxy/xx时将会被分配到该location,此方案相对简单,但是当我们碰到复杂的需求时将无法被满足。

设计方案需求

除了前缀匹配外,我们将会有其它各种需求的匹配:

  • 后缀匹配 比如以wmproxy结尾的path,如/api/update/wmproxy 需要匹配成 *wmproxy
  • 中间匹配 比如常用的api中间转化成数据/api/<user_id>/get,那么匹配为 /api/*/get
  • 正则匹配 当前的配置的为正则规则,需进行匹配
  • 请求方法匹配 比如仅当请求方法为POST才进行转发
  • 客户端IP 比如仅当客户端内网或者外网时区分请求
  • Host地址 比如当前如果请求为ip则不进行转发,需要匹配host才进行转发
  • 协议 比如某个网站不支持http当我们匹配到http时需强制转化成https
    实际配置中当仅仅只有前缀匹配时已经显然无法满足我们的需求

设计方案迭代

当前我们就必须将数据进行更迭,但是在通常情况下我们又不想将配置变得复杂,此时就需要我们支持更多的类的自定义化,首先我们定义类:

/// location匹配,将根据该类的匹配信息进行是否匹配
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Matcher {
    path: Option<String>,
    #[serde_as(as = "Option<DisplayFromStr>")]
    client_ip: Option<IpSets>,
    #[serde_as(as = "Option<DisplayFromStr>")]
    remote_ip: Option<IpSets>,
    host: Option<String>,
    #[serde_as(as = "Option<DisplayFromStr>")]
    method: Option<MatchMethod>,
    #[serde_as(as = "Option<DisplayFromStr>")]
    scheme: Option<MatchScheme>,
}

此时我们将location中的rule的类型从String变成了Matcher,那么此时我们首先遇到的一个问题他可能为一个String值或者可能为一个Map值,我们先得对这种情况进行处理。
我们根据serde的提供的解析方案进行如下函数,当前我们重写了visit_strvisit_map表示我们将只支持这两种源格式转化成Matcher

pub fn string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
    T: Deserialize<'de> + FromStr<Err = WebError>,
    D: Deserializer<'de>,
{
    struct StringOrStruct<T>(PhantomData<fn() -> T>);

    impl<'de, T> Visitor<'de> for StringOrStruct<T>
    where
        T: Deserialize<'de> + FromStr<Err = WebError>,
    {
        type Value = T;

        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
            formatter.write_str("string or map")
        }

        fn visit_str<E>(self, value: &str) -> Result<T, E>
        where
            E: de::Error,
        {
            Ok(FromStr::from_str(value).unwrap())
        }

        fn visit_map<M>(self, map: M) -> Result<T, M::Error>
        where
            M: MapAccess<'de>,
        {
            Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
        }
    }
    deserializer.deserialize_any(StringOrStruct(PhantomData))
}

其次我们将在location中做处理

/// 负载均衡中的location匹配,将匹配合适的处理逻辑
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocationConfig {
    #[serde(deserialize_with = "string_or_struct")]
    pub rule: Matcher,
    //...
}

由于这种大类的匹配通常会在别处额外定义,我们通过以@name@开头来表示索引的信息,来简化配置。通过初始化的时候来重新初始化Matcher

处理匹配

我们初始化完Matcher之后,需要能正确的判断传入的数据是否当前能正确匹配。主要的复杂点在于path的匹配,主要为正则匹配前缀匹配中间匹配后缀匹配
对其进行细分,可确定分为两种

  1. 正则匹配
  2. *的路径匹配
    1. 前缀匹配可以看成/start*或者/start
    2. 中间匹配可以看成/start*end
    3. 后缀匹配可以看成*end

即当前我们只需处理两种匹配模式:

  • 正则匹配,频繁调用时主要在于初始化正则时可能会消耗大量的算力。当前我们对我们的匹配规则的正则进行缓存
/// may memory leak
pub fn try_cache_regex(origin: &str) -> Option<Regex> {
    // 因为均是从配置中读取的数据, 在这里缓存正则表达示会在总量上受到配置的限制
    lazy_static! {
        static ref RE_CACHES: Mutex<HashMap<&'static str, Option<Regex>>> =
            Mutex::new(HashMap::new());
    };

    if origin.len() == 0 {
        return None;
    }

    if let Ok(mut guard) = RE_CACHES.lock() {
        if let Some(re) = guard.get(origin) {
            return re.clone();
        } else {
            if let Ok(re) = Regex::new(origin) {
                guard.insert(
                    Box::leak(origin.to_string().into_boxed_str()),
                    Some(re.clone()),
                );
                return Some(re);
            }
        }
    }
    return None;
}

此处我们用到了static变量,也就是将某部分数据进行了静态化处理,且此处我们将String转化成了&'static str可能存在一定的内存泄漏,大小值跟配置的数据有关,可以接受这空间换取时间。然后用正则的is_match进行匹配即可。

if let Some(re) = Helper::try_cache_regex(&p) {
    if !re.is_match(path) {
        return Ok(false);
    }
}
  • *的路径匹配 主要将路径中的*进行前进字符串的匹配。
    在rust中的字符串切割主要由split或者strip_prefix或者strip_suffix来处理,相对其它语言中均存在的subString或者substr在rust中的则表示为引用,所以在rust中不存在substring函数
let src = "wmproxy is good";
let first = &src[..7];
let second = &src[3..8];
let end = &src[8..];
let vals = src.split(" ").collect::<Vec<&str>>();

以上各数据均引用src的资源,即在这过程中并没有创建内存对象。
那么匹配函数则先将'*'进行分割,数组的第一个则前缀匹配,最后一个则后缀匹配,若不存在'*'则数组数量为1,符合前缀匹配。

pub fn is_match(src: &str, pattern: &str) -> bool {
    let mut oper = src;
    let vals = pattern.split("*").collect::<Vec<&str>>();
    for i in 0..vals.len() {
        if i == 0 {
            if let Some(val) = oper.strip_prefix(vals[i]) {
                oper = val;
            } else {
                return false;
            }
        } else if i == vals.len() - 1 {
            if let Some(val) = oper.strip_suffix(vals[i]) {
                oper = val;
            } else {
                return false;
            }
        } else {
            if let Some(idx) = oper.find(vals[i]) {
                oper = &oper[idx + vals[i].len() .. ]
            } else {
                return false;
            }
        }
    }
    true
}

那么完整的匹配函数在Matcher

/// 当本地限制方法时,优先匹配方法,在进行路径的匹配
pub fn is_match_rule(&self, path: &String, req: &RecvRequest) -> ProtResult<bool>  {
    if let Some(p) = &self.path {
        let mut is_match = false;
        if Helper::is_match(&path, p) {
            is_match = true;
        }
        if !is_match {
            if let Some(re) = Helper::try_cache_regex(&p) {
                if !re.is_match(path) {
                    return Ok(false);
                }
            } else {
                return Ok(false);
            }
        }
    }

    if let Some(m) = &self.method {
        if !m.0.contains(req.method()) {
            return Ok(false);
        }
    }

    if let Some(s) = &self.scheme {
        if !s.0.contains(req.scheme()) {
            return Ok(false);
        }
    }

    if let Some(h) = &self.host {
        match req.get_host() {
            Some(host) if &host == h => {},
            _ => return Ok(false),
        }
    }

    if let Some(c) = &self.client_ip {
        match req.headers().system_get("{client_ip}") {
            Some(ip) => {
                let ip = ip
                .parse::<IpAddr>()
                .map_err(|_| ProtError::Extension("client ip error"))?;
                if !c.contains(&ip) {
                    return Ok(false)
                }
            },
            None => return Ok(false),
        }
    }

    Ok(true)
}

小结

匹配规则在对于复杂匹配的时候尤为重要,我们可以轻松的将各个请求分配到合适的位置,此处我们着重介绍了正则匹配及带*的路径匹配。

点击 [关注][在看][点赞] 是对作者最大的支持

标签:return,47,Some,Ok,nginx,let,匹配,false,Rust
From: https://www.cnblogs.com/wmproxy/p/18000727/wmproxy47

相关文章

  • 原生Nginx文件:/etc/nginx/nginx.conf
    userwww-data;worker_processesauto;pid/run/nginx.pid;include/etc/nginx/modules-enabled/*.conf;events{worker_connections768;#multi_accepton;}http{###BasicSettings##sendfileon;......
  • 搭建Nginx服务器实现WEB服务
    一般搭建Web服务器,都会要求在该服务器上创建几个基于域名的虚拟主机,并且还需要使用DNS实现域名解析,下面内容我们就对这个问题来进行例题的演示。(用2个基于域名虚拟主机)希望能对各位it人士有所帮助,话不多说,我们直接进入主题!!!1.安装Nginxviminstallnginx2.创建所需的站点根目录,在根......
  • PLSQL重命名表的方法和报错解决方法ORA-01765 ORA-14047
    重命名办法在PLSQL中重命名表,在表上点右健选重命名。 报错ORA-01765若不是本用户,会报错:ORA-01765:不允许指定表的所有者名称。 解决方法,使用RENAME命令在PLSQL执行不会报错。示例:ALTERTABLEuser1.log_tablenameRENAMETOlog_tablename_back20240131; 报错ORA-14047......
  • Rust 关于 Cargo 和 Crates.io 的内容
    原文链接参考Rust关于Cargo和Crates.io的内容,注意Windows和Linux系统的文件路径差异。目录采用发布配置自定义构建将crate发布到Crates.io编写有用的文档注释常用(文档注释)部分文档注释作为测试注释包含项的结构使用pubuse导出合适的公有API创建Crates.io账号向新c......
  • nginx代理服务器
    一、Nginx是什么?Nginx(enginex)是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。Nginx是一款轻量级的Web服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,在BSD-like协议下发行。其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网......
  • 1247-jndi-waf
    1247-jndi-waf复现1.判断既然是Json格式自然优先尝试是否是Fastjson.判断是否为Fastjson:删掉末尾的}或添加",使其解析报错,这样就代表的使用的是fastjson。2.查看版本接下来就是探测版本.有两种方法,通过报错和Dnslog.{"@type":"java.lang.AutoCloseable"从返回包可......
  • 1247-jndi
    Fastjson1247-jndi复现过程依旧是探测一下fastjson的版本,使用:{ "@type":"java.lang.AutoCloseable"关于这种方式探测fastjosn版本,条件是需要response中会回显报错信息,但实际环境可能存在不回显的情况,那就需要利用其他手段了那么已知fastjson版本为1.2.47,那么我们就可......
  • ubuntu安装nginx遇到的问题
    执行./configure的时候出现error1.UbuntutheHTTPrewritemodulerequiresthePCRElibrary 缺少pcre 执行sudoapt-getinstalllibpcre3libpcre3-dev sudoapt-getinstallopenssllibssl-dev2.  ./configure:error:theHTTPgzipmodulerequiresthezlibl......
  • nginx-go-crossplane crossplane golang 版本的nginx 配置解析包
    nginx-go-crossplane属于python版本crossplanenginx配置解析包的golang移植可以实现nginx配置解析转换为json格式的数据,当然也支持将json转换为nginx配置格式说明对于希望基于nginx搞自己的流量统一平台,同时希望基于api管理的,nginx-go-crossplane是一个很不错的选择......
  • nginx 配置静态资源认证
    location/media/{set$token$arg_token;if($arg_token=""){return401;}auth_request/validate_token;auth_request_set$auth_status$upstream_status;......