Skip to content

Commit

Permalink
[Release] Add location, rewrite and fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
Bluemangoo authored May 1, 2024
1 parent fb53c10 commit 0c9f8f7
Show file tree
Hide file tree
Showing 8 changed files with 385 additions and 76 deletions.
7 changes: 1 addition & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Release

on:
push:
branches: []
branches: ["master"]
workflow_dispatch:

permissions:
Expand Down Expand Up @@ -31,7 +31,6 @@ jobs:
strategy:
matrix:
target:
- aarch64-apple-ios
- aarch64-apple-darwin
- aarch64-linux-android
- aarch64-unknown-linux-gnu
Expand All @@ -47,10 +46,6 @@ jobs:
- x86_64-unknown-linux-gnu

include:
- target: aarch64-apple-ios
host_os: ubuntu-latest
cross: true

- target: aarch64-apple-darwin
host_os: macos-latest

Expand Down
11 changes: 10 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pingpong"
version = "0.2.0"
version = "0.2.1"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand All @@ -14,3 +14,6 @@ toml = "0.8.12"
serde = { version = "1.0.198", features = ["derive"] }
simplelog = "0.12.2"
anyhow = "1.0.82"
regex = "1.10.4"
http = "1.1.0"
urlencoding = "2.1.3"
53 changes: 49 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Properties of Server

- `thread`: Thread for this server.
- `source`: `Map<String, Source>`. Importable, and each source is also importable.
- `check_status`: Optional, default false, check if source is available, and speedup when unavailable.
- `check_duration`: Optional, default 1000, duration of per status check (ms).

Properties of Source:

Expand All @@ -59,25 +61,68 @@ Properties of Source:
- `host`: Optional, rewrite `Host` in request headers. Fill if upstream service also use sni to recognize route.
- `headers_request`: `Map<String, String>`. Optional and importable, add or replace the header in request.
- `headers_response`: `Map<String, String>`. Optional and importable, add or replace the header in response.
- `location`: Optional, default to match all the requests, see [Location](#location).
- `rewrite`: Optional, see [Rewrite](#rewrite).
- `fallback`: Optional, fallback to other sources when available, only works when `check_status` is enabled.

### Location

Location syntax is similar to [Nginx](https://nginx.org/r/location), but here you provide a list of location pattern.

Matching is after decoding the text encoded in the “%XX” form.

Syntax: `[ = | ^ | ~ ] URI`.

There are three type: `=`(equal), `^`(startsWith) and `~`(regex).

**There is a space between type and URI.**

When no type is provided and uri is starts with `/`, type will be `^`(startsWith).

*Why the hell a URI will start without `/`??*

For example:

```toml
location = ["/public", "~ /static/*.(gif|jpg|jpeg)"]
```

### Rewrite

Rewrite syntax is similar to [Nginx](https://nginx.org/r/rewrite), but here you provide a list of rewrite pattern.

Matching is after decoding the text encoded in the “%XX” form.

Syntax: `rewrite-regex URI [flag]`.

An optional `flag` parameter can be one of:
- `last`: The default one, stops processing the current set of rewrite and starts a search for a new location matching the changed URI;
- `break`: stops processing the current rewrite rule and start to search next.

For example:

```toml
rewrite = ["^/(.*) /service2/$1 last"]
```

## Build

**You can find the latest build in [Actions](https://github.com/Bluemangoo/Pingpong/actions/workflows/build.yml).**
**You can find the latest x86_64 build in [Actions](https://github.com/Bluemangoo/Pingpong/actions/workflows/build.yml).**

Makesure you have cargo and rustc installed.
Make sure you have cargo and rustc installed.

### Build from scratch

```bash
cargo build
```

If successful, you can find the excutable binary here: `target/debug/pingpong`
If successful, you can find the executable binary here: `target/debug/pingpong`

### Build optimised one

```bash
cargo build --release
```

If successful, you can find the excutable binary here: `target/release/pingpong`
If successful, you can find the executable binary here: `target/release/pingpong`
13 changes: 9 additions & 4 deletions config/server.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
threads = 5 # optional.
#sni.cert = "/path/to/cert.pem" # optional.
#sni.key = "/path/to/cert.key" # optional.
#check_status = true # optional, check if source is available, and speedup when unavailable
#check_duration = 1000 # optional, check duration (ms)

[6188.source.service1] # importable structure
ip = "127.0.0.1"
Expand All @@ -12,7 +14,10 @@ ssl = false
ip = "127.18.0.2"
port = 40201
ssl = false
sni = "dev.bluemangoo.net" # optional. The unset one is default.
host = "dev.bluemangoo.net" # optional, rewrite `Host` in request headers.
headers_request = { } # optional and importable, add or replace the header in request
headers_response = { } # optional and importable, add or replace the header in upstream response
sni = "dev.bluemangoo.net" # optional. The unset one is default.
host = "dev.bluemangoo.net" # optional, rewrite `Host` in request headers.
headers_request = { } # optional and importable, add or replace the header in request
headers_response = { } # optional and importable, add or replace the header in upstream response
location = ["/"] # optional, see the documents.
#rewrite = ["^/(.*) /service2/$1 break"] # optional, see the documents
#fallback = ["services1"] # optional, see the documents.
135 changes: 133 additions & 2 deletions src/config/server.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
use crate::config::Importable;
use anyhow::anyhow;
use pingora::lb::health_check;
use pingora::prelude::{background_service, LoadBalancer, RoundRobin};
use regex::Regex;
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;

#[derive(Clone)]
pub struct Source {
pub ip: String,
pub host: Option<String>,
pub port: u16,
pub ssl: bool,
pub load_balancer: Option<Arc<LoadBalancer<RoundRobin>>>,
pub sni: Option<String>,
pub location: Vec<Location>,
pub rewrite: Option<Vec<(Regex, String, RewriteFlag)>>,
pub fallback: Vec<String>,
pub headers_request: Option<HashMap<String, String>>,
pub headers_response: Option<HashMap<String, String>>,
}
Expand All @@ -20,6 +30,9 @@ pub struct SourceRaw {
pub port: u16,
pub ssl: bool,
pub sni: Option<String>,
pub location: Option<Vec<String>>,
pub rewrite: Option<Vec<String>>,
pub fallback: Option<Vec<String>>,
pub headers_request: Option<Importable<HashMap<String, String>>>,
pub headers_response: Option<Importable<HashMap<String, String>>>,
}
Expand All @@ -35,33 +48,74 @@ pub struct Server {
pub source: HashMap<String, Source>,
pub ssl: Option<Ssl>,
pub threads: Option<usize>,
pub check_status: bool,
pub check_duration: u64,
}

#[derive(Deserialize)]
pub struct ServerRaw {
pub source: Importable<HashMap<String, Importable<SourceRaw>>>,
pub ssl: Option<Ssl>,
pub threads: Option<usize>,
pub check_status: Option<bool>,
pub check_duration: Option<u64>,
}

#[derive(Clone)]
pub enum RewriteFlag {
Last,
Break,
}

#[derive(Clone)]
pub enum Location {
Start(String),
Equal(String),
Regex(Regex),
}

impl Server {
pub fn from_raw(raw: ServerRaw, path: &str) -> anyhow::Result<Self> {
let (source_raw, source_path) = raw.source.import(path)?;
let mut source = HashMap::new();
let check_status = raw.check_status.unwrap_or_default();
let check_duration = raw.check_duration.unwrap_or(1000);
for i in source_raw {
let sr = i.1.import(&source_path)?;
source.insert(i.0, Source::from_raw(sr.0, &sr.1)?);
source.insert(
i.0,
Source::from_raw(sr.0, &sr.1, check_status, check_duration)?,
);
}
Ok(Self {
source,
ssl: raw.ssl,
threads: raw.threads,
check_status,
check_duration,
})
}
}

fn into_rewrite(
parts: (&str, &str),
path: &str,
flag: RewriteFlag,
) -> anyhow::Result<(Regex, String, RewriteFlag)> {
Ok((
Regex::new(parts.0).map_err(|err| anyhow!("{} {}", path, err.to_string()))?,
String::from(parts.1),
flag,
))
}

impl Source {
pub fn from_raw(raw: SourceRaw, path: &str) -> anyhow::Result<Self> {
pub fn from_raw(
raw: SourceRaw,
path: &str,
check_status: bool,
check_duration: u64,
) -> anyhow::Result<Self> {
let headers_request = match raw.headers_request {
Some(h) => Some(h.import(path)?.0),
None => None,
Expand All @@ -70,12 +124,89 @@ impl Source {
Some(h) => Some(h.import(path)?.0),
None => None,
};
let load_balancer = if check_status {
let mut upstreams: LoadBalancer<RoundRobin> =
LoadBalancer::try_from_iter([format!("{}{}", &raw.ip, &raw.port)]).unwrap();
let hc = health_check::TcpHealthCheck::new();
upstreams.set_health_check(hc);
upstreams.health_check_frequency = Some(Duration::from_millis(check_duration));
Some(background_service("health check", upstreams).task())
} else {
None
};
let location = match raw.location {
None => vec![Location::Start(String::from("/"))],
Some(loc) => {
let mut result: Vec<Location> = Vec::new();
for location in loc {
result.push(if location.starts_with('/') {
Location::Start(location)
} else {
let parts = location.split(' ').collect::<Vec<&str>>();
if parts.len() != 1 {
Err(anyhow!("{} Wrong syntax: location = {}", path, location))?;
}
if parts[0] == "^" {
Location::Start(if parts[1].ends_with('/') {
String::from(parts[1])
} else {
format!("{}{}", parts[1], '/')
})
} else if parts[0] == "=" {
Location::Equal(String::from(parts[1]))
} else if parts[0] == "~" {
Location::Regex(
Regex::new(parts[1])
.map_err(|err| anyhow!("{} {}", path, err.to_string()))?,
)
} else {
Err(anyhow!("{} Wrong syntax: location = {}", path, location))?
}
})
}
result
}
};
let rewrite = match raw.rewrite {
None => None,
Some(list) => Some({
let mut vec: Vec<(Regex, String, RewriteFlag)> = Vec::new();
list.iter().try_for_each(|v| {
let parts = v.split(' ').collect::<Vec<&str>>();
let res = if parts.len() == 2 {
into_rewrite((parts[0], parts[1]), path, RewriteFlag::Last)
} else if parts.len() == 3 {
if parts[2] == "last" {
into_rewrite((parts[0], parts[1]), path, RewriteFlag::Last)
} else if parts[2] == "break" {
into_rewrite((parts[0], parts[1]), path, RewriteFlag::Break)
} else {
Err(anyhow!("{} Unknown rewrite flag {}", path, parts[3]))
}
} else {
Err(anyhow!("{} Wrong syntax in rewrite: {}", path, v))
};
match res {
Ok(v) => {
vec.push(v);
Ok(())
}
Err(e) => Err(e),
}
})?;
vec
}),
};
Ok(Self {
ip: raw.ip,
host: raw.host,
port: raw.port,
ssl: raw.ssl,
load_balancer,
sni: raw.sni,
location,
rewrite,
fallback: raw.fallback.unwrap_or_default(),
headers_request,
headers_response,
})
Expand Down
Loading

0 comments on commit 0c9f8f7

Please sign in to comment.