39. 干货系列从零用Rust编写负载均衡及代理,正则及格式替换

发布时间 2024-01-09 09:17:39作者: 问蒙服务框架

wmproxy

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

项目地址

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

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

项目设计目标

利用正则替换的能力,能把指定的字符串替换成想要的字符串。

正则库

因为rust官方团队并未将正则正式的加入到std标准库里面,目前我们引用的是regex也是rust-lang官方出品的正则库。

匹配的规则

  1. 字符匹配:正则表达式可以匹配单个字符,如字母、数字、标点符号等。常见的字符匹配包括:
  • \d:匹配任意数字,等价于[0-9]
  • \D:匹配任意非数字字符,等价于[^0-9]
  • \w:匹配任意字母、数字或下划线字符,等价于[A-Za-z0-9_]
  • \W:匹配任意非字母、数字或下划线字符,等价于[^A-Za-z0-9_]
  • .:匹配除换行符(\n、\r)之外的任意字符。
  1. 字符类匹配:使用字符类可以匹配指定范围内的字符。常见的字符类匹配包括:
  • [abc]:匹配方括号内的任意字符,例如abc
  • [^abc]:匹配除方括号内字符之外的任意字符,例如不是abc的字符。
  • [a-z]:匹配任意小写字母。
  • [A-Z]:匹配任意大写字母。
  • [0-9]:匹配任意数字。
  1. 量词匹配:用于指定字符或字符类出现的次数。常见的量词匹配包括:
  • *:匹配前一项0次或多次,等价于{0,}
  • +:匹配前一项1次或多次,等价于{1,}
  • ?:匹配前一项0次或1次,也就是说前一项是可选的,等价于{0,1}
  • {n}:匹配前一项恰好n次。
  • {n,}:匹配前一项至少n次。
  • {n,m}:匹配前一项至少n次,但不超过m次。
  1. 边界匹配:用于匹配字符串的边界位置。常见的边界匹配包括:
  • ^:匹配字符串的开头位置。
  • $:匹配字符串的结尾位置。
  • \b:匹配单词的边界位置,即字与空白间的位置。
  • \B:匹配非单词边界的位置。
  1. 选择、分组和引用:
  • |:选择符号,匹配该符号左边或右边的表达式。
  • (...):将几项组合成一个单元,这个单元可通过"*"、"+"、"?" 和"|" 等符号加以修饰,也可以记住与这个组匹配的字符以便后面引用。
  • \n:在正则表达式中,n 是一个正整数,引用匹配到的第n个分组。
  1. 预查:预查是一种零宽断言,即匹配的是位置而不是字符。预查包括正向预查和负向预查:
  • (?=...):正向肯定预查,表示要匹配的字符串后面必须紧跟着指定的模式。
  • (?!...):正向否定预查,表示要匹配的字符串后面不能紧跟着指定的模式。
  • (?<=...):反向肯定预查,表示要匹配的字符串前面必须紧跟着指定的模式。
  • (?<!...):反向否定预查,表示要匹配的字符串前面不能紧跟着指定的模式。

需求功能

  • 需要从Request中获取Url或者Path并将其中间的某一部分替换成另一部分,且需兼容部分不需要替换,两种模式需均能正常的。

  • 需要配置中正确的读取分割的信息,如"{path}/ '/ro(\\w+)/(.*) {path} /ro$1/Cargo.toml' /root/README.md"需要正确的分割成{path}/, /ro(\\w+)/(.*) {path} /ro$1/Cargo.toml, /root/README.md三个部分。

需求实现

以下是一段try_paths的配置

[[http.server.location]]
rate_limit = "4m/s"
rule = "/root"
file_server = { browse = true }
proxy_pass = ""
try_paths = "{path}/ '/ro(\\w+)/(.*) {path} /ro$1/Cargo.toml' /root/README.md"

我们需要将try_paths做正确的拆分,我们需要将一个字符串按空格做分割,且如果有单引号'或者双引号"需要找到其对应的结尾,防止将其中的字符串做切割。

我们将利用以下正则,其中小括号括起来是我们将匹配的内容,用|则表示并行的匹配规则,当第一个没有匹配到将匹配第二个选项。

r#"([^\s'"]+)|"([^"]*)"|'([^']*)'"#

其中([^\s'"]+)表示非空白字符开头也非单号双引号开头,直到碰到空格或者单引号双引号停止,那我们将获取第一个匹配项{path}/

其中"([^"]*)"则表示以双引号开头,中间不能添加任何双引号的其它任意字符,直到匹配到另一个双引号停止,取中间的数据,不取双引号。'([^']*)'则类似双引号,那么'/ro(\\w+)/(.*) {path} /ro$1/Cargo.toml'将是单引号开头的匹配项直到另一个单引号截止,那么匹配的结果为/ro(\\w+)/(.*) {path} /ro$1/Cargo.toml

另一个匹配第一个规则/root/README.md,此时我们已经正确将数据进行切割,以下是源码实现,用lazy_static是为了只初始化一次,无需重复耗性能。

pub fn split_by_whitespace<'a>(key: &'a str) -> Vec<&'a str> {
    lazy_static! {
        static ref RE: Regex = Regex::new(r#"([^\s'"]+)|"([^"]*)"|'([^']*)'"#).unwrap();
    };
    
    let mut vals = vec![];
    for (_, [value]) in RE.captures_iter(key).map(|c| c.extract()) {
        vals.push(value);
    }
    vals
}

以下是正确的替换,假设我们收到的是
GET /root/index.html HTTP/1.1\r\n
那么第一个参数{path}/将通过Request取得为/root/index.html/

那么第二个参数/ro(\\w+)/(.*) {path} /ro$1/Cargo.toml,按空格切割,切割结果有/ro(\\w+)/(.*),{path},/ro$1/Cargo.toml有三个参数,且第一个参数为正则,那么我们尝试将第一个参数正则化,与第二个参数相匹配,并替换成第三个参数的内容同时将第二个参数格式化为/root/index.html,那么与正则相匹配

匹配项 匹配结果
$0 /root/index.html
正则表达示里0均为整个字符串
$1 ot
(\w+)的内容
$2 index.html
(.*)的内容

那么/ro$1/Cargo.toml替换成结果后将为/root/Cargo.toml通过这种方法我们可以将任意的字符串按照一定的规则匹配成另一个字符串来达到自定义的目的。

源码实现:

pub fn format_req(req: &Request<Body>, formats: &str) -> String {
    let pw = FORMAT_PATTERN_CACHE.with(|m| {
        if !m.borrow().contains_key(&formats) {
            let p = PatternEncoder::new(formats);
            m.borrow_mut().insert(
                Box::leak(formats.to_string().clone().into_boxed_str()),
                Arc::new(p),
            );
        }
        m.borrow()[&formats].clone()
    });

    // 将其转化成Record然后进行encode
    let record = ProxyRecord::new_req(Record::builder().level(Level::Info).build(), req);
    let mut buf = vec![];
    pw.encode(&mut SimpleWriter(&mut buf), &record).unwrap();
    String::from_utf8_lossy(&buf[..]).to_string()
}

fn inner_oper_regex(req: &Request<Body>, re: &Regex, vals: &[&str]) -> String {
    let mut ret = String::new();
    let key = Self::format_req(req, vals[0]);
    for idx in 1..vals.len() {
        if idx != 1 {
            ret += " ";
        }
        let val = re.replace_all(&key, vals[idx]);
        ret += &val;
    }
    ret
}

pub fn format_req_may_regex(req: &Request<Body>, formats: &str) -> String {
    let formats = formats.trim();
    if formats.contains(char::is_whitespace) {
        // 因为均是从配置中读取的数据, 在这里缓存正则表达示会在总量上受到配置的限制
        lazy_static! {
            static ref RE_CACHES: Mutex<HashMap<&'static str, Regex>> =
                Mutex::new(HashMap::new());
        };

        if formats.len() == 0 {
            return String::new();
        }

        let vals = Self::split_by_whitespace(formats);
        if vals.len() < 2 {
            return String::new();
        }

        if let Ok(mut guard) = RE_CACHES.lock() {
            if let Some(re) = guard.get(&vals[1]) {
                return Self::inner_oper_regex(req, re, &vals[1..]);
            } else {
                if let Ok(re) = Regex::new(vals[0]) {
                    let ret = Self::inner_oper_regex(req, &re, &vals[1..]);
                    guard.insert(Box::leak(vals[0].to_string().into_boxed_str()), re);
                    return ret;
                }
            }
        }
    }
    Self::format_req(req, formats)
}

测试用例

根据Request生成我们任意想要的内容

mod tests {
    use webparse::Request;
    use wenmeng::Body;

    use crate::Helper;

    fn build_request() -> Request<Body> {
        Request::builder()
            .url("http://127.0.0.1/test/root?query=1&a=b")
            .header("Accept", "text/html")
            .body("ok")
            .unwrap()
            .into_type()
    }

    #[test]
    fn do_test_reg() {
        let req = &build_request();
        let format = r" /test/(.*) {path} /formal/$1 ";
        let val = Helper::format_req_may_regex(req, format);
        assert_eq!(val, "/formal/root");
        
        let format = r" /te(\w+)/(.*) {path} /formal/$1/$2 ";
        let val = Helper::format_req_may_regex(req, format);
        assert_eq!(val, "/formal/st/root");

        let format = r" /te(\w+)/(.*) {url} /formal/$1/$2 ";
        let val = Helper::format_req_may_regex(req, format);
        assert_eq!(val, "http://127.0.0.1/formal/st/root?query=1&a=b");
    }
}

小结

正则在计算机的处理中是非常的常用的一种技术,具有许多优点,使得它在文本处理和模式匹配方面非常强大和灵活,有强大的文本匹配和搜索功能,跨平台性跨语言,每种语言都有相应的实现,既简洁又高效便捷,是受欢迎的一种又相处较难的字符串处理技术。

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