Compare commits
No commits in common. "dev" and "main" have entirely different histories.
25 changed files with 1 additions and 5389 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +0,0 @@
|
||||||
/target
|
|
||||||
examples/*/target
|
|
|
@ -1,10 +0,0 @@
|
||||||
when:
|
|
||||||
- event: push
|
|
||||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: 'audit dependencies'
|
|
||||||
image: rust:1.88
|
|
||||||
commands:
|
|
||||||
- cargo install cargo-audit
|
|
||||||
- cargo audit
|
|
|
@ -1,9 +0,0 @@
|
||||||
when:
|
|
||||||
- event: push
|
|
||||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: 'Check code'
|
|
||||||
image: rust:1.88
|
|
||||||
commands:
|
|
||||||
- cargo check
|
|
|
@ -1,19 +0,0 @@
|
||||||
when:
|
|
||||||
- event: push
|
|
||||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: 'Format w/ Cargo'
|
|
||||||
image: rust:1.88
|
|
||||||
commands:
|
|
||||||
- rustup component add rustfmt
|
|
||||||
- cargo fmt
|
|
||||||
|
|
||||||
- name: 'Push formatted'
|
|
||||||
image: appleboy/drone-git-push
|
|
||||||
settings:
|
|
||||||
remote_name: origin
|
|
||||||
branch: ${CI_COMMIT_BRANCH}
|
|
||||||
commit: true
|
|
||||||
commit_message: "[skip ci] chore: automated code formatting"
|
|
||||||
author_name: "Woodpecker CI"
|
|
1472
Cargo.lock
generated
1472
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
28
Cargo.toml
28
Cargo.toml
|
@ -1,28 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "boxy"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
hyper = { version = "1.6.0", features = ["full"]}
|
|
||||||
tokio = { version = "1.45.1", features = ["full"]}
|
|
||||||
http-body-util = "0.1.3"
|
|
||||||
hyper-util = { version = "0.1.14", features = ["full"]}
|
|
||||||
log = "0.4.27"
|
|
||||||
pretty_env_logger = "0.5.0"
|
|
||||||
anyhow = "1.0.98"
|
|
||||||
thiserror = "2.0.12"
|
|
||||||
tokio-postgres = "0.7.13"
|
|
||||||
bcrypt = "0.17.0"
|
|
||||||
ring = "0.17.14"
|
|
||||||
nanoid = "0.4.0"
|
|
||||||
serde_yaml_bw = "2.1.1"
|
|
||||||
serde = "1.0.219"
|
|
||||||
base64 = "0.22.1"
|
|
||||||
string-builder = "0.2.0"
|
|
||||||
json = "0.12.4"
|
|
||||||
ansi_colours = "1.2.3"
|
|
||||||
colour = "2.1.0"
|
|
||||||
async-trait = "0.1.88"
|
|
||||||
http = "1.3.1"
|
|
||||||
|
|
|
@ -1,8 +1,2 @@
|
||||||
# Boxy
|
# boxy
|
||||||
|
|
||||||
The reverse reverse proxy.
|
|
||||||
|
|
||||||
## Todo
|
|
||||||
|
|
||||||
- [ ] Automatic SSL certificates (LE)
|
|
||||||
|
|
||||||
|
|
13
config.yaml
13
config.yaml
|
@ -1,13 +0,0 @@
|
||||||
database: 'postgresql://postgres:trust@127.0.0.1'
|
|
||||||
|
|
||||||
proxy:
|
|
||||||
listen: 127.0.0.1
|
|
||||||
port: 8005
|
|
||||||
|
|
||||||
api:
|
|
||||||
listen: 127.0.0.1
|
|
||||||
port: 8006
|
|
||||||
|
|
||||||
clients:
|
|
||||||
- name: 'eu-central-1' # Example Client right here (the client in this case would be for example the stereo.cat backend)
|
|
||||||
hashed_secret: '$2b$12$5wH/0p702PPqVp7fCpVS4.1GA2/wAbk89w2nMjwuS8439OhjCUGbK' # password123
|
|
2615
examples/example-server/Cargo.lock
generated
2615
examples/example-server/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,9 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "example-server"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
reqwest = { version = "0.12.22", features = ["json", "blocking"] }
|
|
||||||
rocket = "0.5.1"
|
|
||||||
serde_json = "1.0.141"
|
|
|
@ -1,60 +0,0 @@
|
||||||
use std::{collections::HashMap, fmt::format};
|
|
||||||
|
|
||||||
use rocket::{build, get, launch, routes};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
extern crate rocket;
|
|
||||||
|
|
||||||
const BOXY_ADDRESS: &str = "localhost";
|
|
||||||
const BOXY_PORT: u16 = 8006;
|
|
||||||
|
|
||||||
const CLIENT_NAME: &str = "eu-central-1";
|
|
||||||
const CLIENT_SECRET: &str = "password123";
|
|
||||||
|
|
||||||
#[get("/")]
|
|
||||||
fn index() -> &'static str {
|
|
||||||
"Hello world!"
|
|
||||||
}
|
|
||||||
|
|
||||||
#[launch]
|
|
||||||
fn rocket() -> _ {
|
|
||||||
// TODO: clean up example code
|
|
||||||
|
|
||||||
// This is the example backend client. (The stereo.cat backend for example).
|
|
||||||
let client = reqwest::blocking::Client::new();
|
|
||||||
|
|
||||||
// We define the port of the server running locally and the hostname we want to route to it.
|
|
||||||
let body = json!({
|
|
||||||
"port": 8000,
|
|
||||||
"hosts": ["localhost:8005"],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send it to Boxy's API
|
|
||||||
let res = client
|
|
||||||
.post(format!("http://{}:{}/register", BOXY_ADDRESS, BOXY_PORT))
|
|
||||||
.basic_auth(CLIENT_NAME, Some(CLIENT_SECRET))
|
|
||||||
.json(&body)
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let id = res.text().unwrap();
|
|
||||||
|
|
||||||
println!("{}", id);
|
|
||||||
|
|
||||||
let body2 = json!({});
|
|
||||||
|
|
||||||
// Send it to Boxy's API
|
|
||||||
let res2 = client
|
|
||||||
.delete(format!(
|
|
||||||
"http://{}:{}/endpoint/{}",
|
|
||||||
BOXY_ADDRESS, BOXY_PORT, id
|
|
||||||
))
|
|
||||||
.basic_auth(CLIENT_NAME, Some(CLIENT_SECRET))
|
|
||||||
.json(&body2)
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
println!("{}", res2.text().unwrap());
|
|
||||||
|
|
||||||
build().mount("/", routes![index])
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
use std::error::Error;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tokio::{fs::File, io::AsyncReadExt};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
|
||||||
pub struct Proxy {
|
|
||||||
pub listen: String,
|
|
||||||
pub port: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
|
||||||
pub struct Db {
|
|
||||||
pub host: String,
|
|
||||||
pub user: String,
|
|
||||||
pub port: Option<u16>,
|
|
||||||
pub password: Option<String>,
|
|
||||||
pub database: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
|
||||||
pub struct Api {
|
|
||||||
pub listen: String,
|
|
||||||
pub port: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
|
||||||
pub struct Client {
|
|
||||||
pub name: String,
|
|
||||||
pub hashed_secret: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
|
||||||
pub async fn verify(us: String, config: Config) -> bool {
|
|
||||||
// us stands for user:secret btw
|
|
||||||
let us_split: Vec<&str> = us.split(':').collect();
|
|
||||||
|
|
||||||
let name = us_split.first().unwrap();
|
|
||||||
let secret = us_split.last().unwrap();
|
|
||||||
|
|
||||||
let client: Client = config
|
|
||||||
.clients
|
|
||||||
.into_iter()
|
|
||||||
.filter(|x| x.name.eq(name))
|
|
||||||
.nth(0)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
bcrypt::verify(secret, client.hashed_secret.as_str()).unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
|
||||||
pub struct Config {
|
|
||||||
pub database: String,
|
|
||||||
pub proxy: Proxy,
|
|
||||||
pub api: Api,
|
|
||||||
pub clients: Vec<Client>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub async fn get() -> Result<Self, Box<dyn Error>> {
|
|
||||||
let mut file = File::open("./config.yaml").await?;
|
|
||||||
let mut contents = String::new();
|
|
||||||
|
|
||||||
file.read_to_string(&mut contents).await?;
|
|
||||||
|
|
||||||
let config: Self = serde_yaml_bw::from_str::<Self>(&contents)?;
|
|
||||||
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
}
|
|
210
src/db.rs
210
src/db.rs
|
@ -1,210 +0,0 @@
|
||||||
use std::{error::Error, net::IpAddr};
|
|
||||||
|
|
||||||
use log::warn;
|
|
||||||
use tokio_postgres::{Client, Row};
|
|
||||||
|
|
||||||
const ENDPOINT_TABLE: &str = "endpoints";
|
|
||||||
const HOSTS_RELATION_TABLE: &str = "hosts";
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct BoxyDatabase {
|
|
||||||
pub client: Client,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Endpoint {
|
|
||||||
pub id: Option<u32>,
|
|
||||||
pub address: IpAddr,
|
|
||||||
pub port: u16,
|
|
||||||
pub callback: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Endpoint {
|
|
||||||
pub async fn new(id: Option<u32>, address: IpAddr, port: u16, callback: String) -> Self {
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
address,
|
|
||||||
port,
|
|
||||||
callback,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub async fn get_by_hostname(
|
|
||||||
db: &BoxyDatabase,
|
|
||||||
hostname: String,
|
|
||||||
) -> Result<Vec<Self>, Box<dyn Error>> {
|
|
||||||
let mut result: Vec<Self> = Vec::new();
|
|
||||||
|
|
||||||
let rows = db
|
|
||||||
.client
|
|
||||||
.query(
|
|
||||||
format!(
|
|
||||||
"SELECT {ENDPOINT_TABLE}.* FROM {HOSTS_RELATION_TABLE}
|
|
||||||
JOIN {ENDPOINT_TABLE} ON {HOSTS_RELATION_TABLE}.endpoint_id = {ENDPOINT_TABLE}.id
|
|
||||||
WHERE {HOSTS_RELATION_TABLE}.hostname = $1"
|
|
||||||
).as_str(),
|
|
||||||
&[&hostname],
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
for row in rows {
|
|
||||||
result.push(row.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
pub async fn get_all(db: &BoxyDatabase) -> Result<Vec<Self>, Box<dyn Error>> {
|
|
||||||
let mut result: Vec<Self> = Vec::new();
|
|
||||||
|
|
||||||
let rows = db
|
|
||||||
.client
|
|
||||||
.query(format!("SELECT * FROM {ENDPOINT_TABLE}").as_str(), &[])
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
for row in rows {
|
|
||||||
result.push(row.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
pub async fn get_by_id(db: &BoxyDatabase, id: u32) -> Result<Option<Self>, Box<dyn Error>> {
|
|
||||||
let endpoint = db
|
|
||||||
.client
|
|
||||||
.query_one(
|
|
||||||
format!("SELECT * FROM {ENDPOINT_TABLE} WHERE id = $1").as_str(),
|
|
||||||
&[&(id as i32)],
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Some(endpoint.into()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Row> for Endpoint {
|
|
||||||
fn from(value: Row) -> Self {
|
|
||||||
Self {
|
|
||||||
id: Some(value.get::<&str, i32>("id") as u32),
|
|
||||||
address: value.get("address"),
|
|
||||||
port: value.get::<&str, i32>("port") as u16,
|
|
||||||
callback: value.get("callback"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Endpoint {
|
|
||||||
pub async fn host(
|
|
||||||
&self,
|
|
||||||
db: &mut BoxyDatabase,
|
|
||||||
hostnames: Vec<String>,
|
|
||||||
) -> Result<(), tokio_postgres::Error> {
|
|
||||||
let tx = db.client.transaction().await?;
|
|
||||||
|
|
||||||
let statement = tx
|
|
||||||
.prepare(
|
|
||||||
format!("INSERT INTO {HOSTS_RELATION_TABLE} (endpoint_id,hostname) VALUES ($1,$2)")
|
|
||||||
.as_str(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
for host in hostnames {
|
|
||||||
tx.execute(&statement, &[&(self.id.unwrap() as i32), &host])
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.commit().await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub async fn delete(self, db: &mut BoxyDatabase) -> Result<(), tokio_postgres::Error> {
|
|
||||||
let tx = db.client.transaction().await?;
|
|
||||||
|
|
||||||
let id = self.id.unwrap() as i32;
|
|
||||||
|
|
||||||
tx.execute(
|
|
||||||
format!("DELETE FROM {ENDPOINT_TABLE} where id = $1").as_str(),
|
|
||||||
&[&id],
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
tx.commit().await?;
|
|
||||||
|
|
||||||
warn!(
|
|
||||||
"Removed endpoint with ID: {}, address: {}:{}",
|
|
||||||
id, self.address, self.port
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub async fn register(
|
|
||||||
&mut self,
|
|
||||||
db: &mut BoxyDatabase,
|
|
||||||
hostnames: Vec<String>,
|
|
||||||
) -> Result<(), tokio_postgres::Error> {
|
|
||||||
let tx = db.client.transaction().await?;
|
|
||||||
|
|
||||||
let endpoint_id: i32 = tx
|
|
||||||
.query_one(
|
|
||||||
format!(
|
|
||||||
"INSERT INTO {ENDPOINT_TABLE} (address,port,callback) VALUES ($1, $2, $3) RETURNING id").as_str(),
|
|
||||||
&[
|
|
||||||
&self.address,
|
|
||||||
&(self.port as i32),
|
|
||||||
&self.callback,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
.get("id");
|
|
||||||
|
|
||||||
let statement = tx
|
|
||||||
.prepare(
|
|
||||||
format!("INSERT INTO {HOSTS_RELATION_TABLE} (endpoint_id,hostname) VALUES ($1,$2)")
|
|
||||||
.as_str(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
for host in hostnames {
|
|
||||||
tx.execute(&statement, &[&endpoint_id, &host]).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.commit().await?;
|
|
||||||
|
|
||||||
self.id = Some(endpoint_id as u32);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BoxyDatabase {
|
|
||||||
pub async fn new(c: Client) -> Result<Self, Box<dyn Error>> {
|
|
||||||
c.execute(
|
|
||||||
format!(
|
|
||||||
"CREATE TABLE IF NOT EXISTS {ENDPOINT_TABLE}
|
|
||||||
(
|
|
||||||
id serial PRIMARY KEY,
|
|
||||||
address inet NOT NULL,
|
|
||||||
port integer CHECK (port >= 0 AND port <= 65535) NOT NULL,
|
|
||||||
callback text
|
|
||||||
)
|
|
||||||
"
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
c.execute(
|
|
||||||
format!(
|
|
||||||
"CREATE TABLE IF NOT EXISTS {HOSTS_RELATION_TABLE}
|
|
||||||
(
|
|
||||||
id serial PRIMARY KEY,
|
|
||||||
endpoint_id int REFERENCES {ENDPOINT_TABLE}(id) ON DELETE CASCADE,
|
|
||||||
hostname text
|
|
||||||
)
|
|
||||||
"
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(BoxyDatabase { client: c })
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
use http_body_util::Empty;
|
|
||||||
use hyper::{Request, body::Bytes};
|
|
||||||
use hyper_util::rt::TokioIo;
|
|
||||||
use log::error;
|
|
||||||
use tokio::net::TcpStream;
|
|
||||||
|
|
||||||
use crate::db::{BoxyDatabase, Endpoint};
|
|
||||||
|
|
||||||
pub async fn check(db: &mut BoxyDatabase) {
|
|
||||||
let endpoints = Endpoint::get_all(db).await.unwrap();
|
|
||||||
|
|
||||||
for endpoint in endpoints {
|
|
||||||
let address = format!("{}:{}", endpoint.address, endpoint.port);
|
|
||||||
|
|
||||||
let url = format!("http://{}{}", address, endpoint.callback)
|
|
||||||
.parse::<hyper::Uri>()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let stream = match TcpStream::connect(address).await {
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Could not reach endpoint {}: {e}", endpoint.id.unwrap());
|
|
||||||
|
|
||||||
endpoint.delete(db).await.unwrap();
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let io = TokioIo::new(stream);
|
|
||||||
|
|
||||||
let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap();
|
|
||||||
|
|
||||||
tokio::task::spawn(async move {
|
|
||||||
if let Err(err) = conn.await {
|
|
||||||
println!("Connection failed: {:?}", err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let req = Request::builder()
|
|
||||||
.uri(url)
|
|
||||||
.body(Empty::<Bytes>::new())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let res = match sender.send_request(req).await {
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Could not reach endpoint {}: {e}", endpoint.id.unwrap());
|
|
||||||
|
|
||||||
endpoint.delete(db).await.unwrap();
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !res.status().is_success() {
|
|
||||||
endpoint.delete(db).await.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
111
src/main.rs
111
src/main.rs
|
@ -1,111 +0,0 @@
|
||||||
mod config;
|
|
||||||
mod db;
|
|
||||||
mod health;
|
|
||||||
mod matchers;
|
|
||||||
mod routes;
|
|
||||||
mod server;
|
|
||||||
mod services;
|
|
||||||
|
|
||||||
use std::{env, process::exit, sync::Arc, time::Duration};
|
|
||||||
|
|
||||||
use bcrypt::DEFAULT_COST;
|
|
||||||
use config::Config;
|
|
||||||
use db::BoxyDatabase;
|
|
||||||
use log::{debug, error, info};
|
|
||||||
use matchers::api::ApiMatcher;
|
|
||||||
use server::Server;
|
|
||||||
use services::{controller::ControllerService, matcher::Matcher};
|
|
||||||
use tokio::{
|
|
||||||
sync::Mutex,
|
|
||||||
time::{self},
|
|
||||||
};
|
|
||||||
use tokio_postgres::NoTls;
|
|
||||||
|
|
||||||
const VERSION: &str = "v0.1a";
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
if env::var("RUST_LOG").is_err() {
|
|
||||||
unsafe { env::set_var("RUST_LOG", "info") };
|
|
||||||
}
|
|
||||||
|
|
||||||
pretty_env_logger::init();
|
|
||||||
|
|
||||||
let args: Vec<String> = env::args().collect();
|
|
||||||
|
|
||||||
if args.len() > 1 {
|
|
||||||
match args[1].as_str() {
|
|
||||||
"hash" => {
|
|
||||||
if args.len() < 3 {
|
|
||||||
error!("You need to specify a string to hash.");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let hash = bcrypt::hash(&args[2], DEFAULT_COST).unwrap();
|
|
||||||
info!("Generated Hash: {}", hash);
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
"version" => {
|
|
||||||
info!("Version: {}", VERSION);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = Config::get().await.unwrap();
|
|
||||||
|
|
||||||
debug!("Database URI: {}", config.database);
|
|
||||||
|
|
||||||
let (client, conn) = tokio_postgres::connect(config.database.as_str(), NoTls)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(e) = conn.await {
|
|
||||||
error!("Error while connecting to database: {}", e);
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
info!("Connected to database.");
|
|
||||||
|
|
||||||
let database = Box::new(BoxyDatabase::new(client).await.unwrap());
|
|
||||||
|
|
||||||
let database_shared = Arc::new(Mutex::new(Box::leak(database)));
|
|
||||||
|
|
||||||
let api_matcher = ApiMatcher::new(database_shared.clone(), config.clone()).await;
|
|
||||||
|
|
||||||
let svc = ControllerService {
|
|
||||||
database: database_shared.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let api_server = Server::new(api_matcher.service(), (config.api.listen, config.api.port))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let proxy_server = Server::new(svc, (config.proxy.listen, config.proxy.port))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
tokio::task::spawn(async move {
|
|
||||||
info!("Starting API server...");
|
|
||||||
api_server.handle().await;
|
|
||||||
});
|
|
||||||
|
|
||||||
tokio::task::spawn(async move {
|
|
||||||
let mut interval = time::interval(Duration::from_secs(30));
|
|
||||||
|
|
||||||
loop {
|
|
||||||
health::check(*database_shared.lock().await).await;
|
|
||||||
interval.tick().await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// We don't put this on a separate thread because we'd be wasting the main thread.
|
|
||||||
info!("Starting proxy server...");
|
|
||||||
proxy_server.handle().await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod api;
|
|
|
@ -1,175 +0,0 @@
|
||||||
use std::{net::IpAddr, sync::Arc};
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use base64::{Engine, prelude::BASE64_STANDARD};
|
|
||||||
use http_body_util::BodyExt;
|
|
||||||
use hyper::{Request, StatusCode, body::Incoming};
|
|
||||||
use json::JsonValue;
|
|
||||||
use log::{debug, error, warn};
|
|
||||||
use tokio::{net::TcpStream, sync::Mutex};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
config::{Client, Config},
|
|
||||||
db::BoxyDatabase,
|
|
||||||
routes::api::{AddHost, RegisterEndpoint, RemoveHost},
|
|
||||||
server::{GeneralResponse, custom_resp},
|
|
||||||
services::matcher::Matcher,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ApiMatcher {
|
|
||||||
pub database: Arc<Mutex<&'static mut BoxyDatabase>>,
|
|
||||||
pub config: Config,
|
|
||||||
pub _address: Option<IpAddr>,
|
|
||||||
pub body: Option<JsonValue>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApiMatcher {
|
|
||||||
pub async fn new(database: Arc<Mutex<&'static mut BoxyDatabase>>, config: Config) -> Self {
|
|
||||||
Self {
|
|
||||||
database,
|
|
||||||
config,
|
|
||||||
_address: None,
|
|
||||||
body: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Matcher for ApiMatcher {
|
|
||||||
async fn unimatch(
|
|
||||||
&mut self,
|
|
||||||
req: &Request<Incoming>,
|
|
||||||
) -> (bool, Option<Result<GeneralResponse, hyper::Error>>) {
|
|
||||||
let address = self._address.unwrap();
|
|
||||||
|
|
||||||
let encoded_header = match req.headers().get(hyper::header::AUTHORIZATION) {
|
|
||||||
None => {
|
|
||||||
error!("Authorization header not given for request from {address}",);
|
|
||||||
|
|
||||||
return (
|
|
||||||
false,
|
|
||||||
Some(Ok(custom_resp(
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
"Invalid credentials.".to_string(),
|
|
||||||
)
|
|
||||||
.await)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some(x) => x,
|
|
||||||
}
|
|
||||||
.to_str()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
debug!("authorization header: {}", encoded_header);
|
|
||||||
|
|
||||||
let auth_bytes = match BASE64_STANDARD.decode(&encoded_header[6..]) {
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error while decoding authorization header from {address}: {e}",);
|
|
||||||
|
|
||||||
return (
|
|
||||||
false,
|
|
||||||
Some(Ok(custom_resp(
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
"Invalid base64 string given.".to_string(),
|
|
||||||
)
|
|
||||||
.await)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let auth_string = match String::from_utf8(auth_bytes) {
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error while decoding authorization header from {address}: {e}",);
|
|
||||||
|
|
||||||
return (
|
|
||||||
false,
|
|
||||||
Some(Ok(custom_resp(
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
"Invalid UTF-8 in body.".to_string(),
|
|
||||||
)
|
|
||||||
.await)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!("decoded auth string: {}", auth_string);
|
|
||||||
|
|
||||||
if !Client::verify(auth_string.clone(), self.config.clone()).await {
|
|
||||||
warn!(
|
|
||||||
"Authentication for string {} from {} failed.",
|
|
||||||
auth_string, address
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
false,
|
|
||||||
Some(Ok(custom_resp(
|
|
||||||
StatusCode::UNAUTHORIZED,
|
|
||||||
"Invalid credentials.".to_string(),
|
|
||||||
)
|
|
||||||
.await)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (true, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn retrieve(&self) -> Vec<Arc<dyn crate::services::matcher::Route<Self> + Sync + Send>> {
|
|
||||||
vec![
|
|
||||||
Arc::new(RegisterEndpoint {}),
|
|
||||||
Arc::new(AddHost {}),
|
|
||||||
Arc::new(RemoveHost {}),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stream(&mut self, stream: &TcpStream) {
|
|
||||||
self._address = Some(stream.peer_addr().unwrap().ip());
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn body(&mut self, body: Incoming) -> Option<Result<GeneralResponse, hyper::Error>> {
|
|
||||||
let address = self._address.unwrap();
|
|
||||||
|
|
||||||
let body_string = match String::from_utf8(
|
|
||||||
body.collect()
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.to_bytes()
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<u8>>()
|
|
||||||
.clone(),
|
|
||||||
) {
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error while inferring UTF-8 string from {address}'s request body: {e}",);
|
|
||||||
|
|
||||||
return Some(Ok(custom_resp(
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
"Invalid UTF-8 in body.".to_string(),
|
|
||||||
)
|
|
||||||
.await));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!("body: {}", body_string);
|
|
||||||
|
|
||||||
let json = match json::parse(body_string.as_str()) {
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error while parsing JSON body from {address}: {e}",);
|
|
||||||
|
|
||||||
return Some(Ok(custom_resp(
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
"Invalid JSON in body.".to_string(),
|
|
||||||
)
|
|
||||||
.await));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.body = Some(json);
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod api;
|
|
|
@ -1,154 +0,0 @@
|
||||||
use async_trait::async_trait;
|
|
||||||
use http::request::Parts;
|
|
||||||
use hyper::{Method, StatusCode};
|
|
||||||
use log::error;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
db::Endpoint,
|
|
||||||
matchers::api::ApiMatcher,
|
|
||||||
server::{custom_resp, json_to_vec},
|
|
||||||
services::matcher::Route,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct AddHost {}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Route<ApiMatcher> for AddHost {
|
|
||||||
fn matcher(&self, _: &ApiMatcher, req: &hyper::Request<hyper::body::Incoming>) -> bool {
|
|
||||||
req.uri().path().starts_with("/endpoint/") && req.method() == Method::POST
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn call(
|
|
||||||
&self,
|
|
||||||
m: &ApiMatcher,
|
|
||||||
parts: Parts,
|
|
||||||
) -> Result<crate::server::GeneralResponse, hyper::Error> {
|
|
||||||
let database = m.database.clone();
|
|
||||||
let address = m._address.unwrap();
|
|
||||||
let body = m.body.clone().unwrap();
|
|
||||||
|
|
||||||
let endpoint_id: u32 = parts.uri.path().replace("/endpoint/", "").parse().unwrap();
|
|
||||||
|
|
||||||
if !body["hosts"].is_array() {
|
|
||||||
error!("Hosts parameter is not an array.",);
|
|
||||||
|
|
||||||
return Ok(custom_resp(
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
"Hosts parameter is not an array.".to_string(),
|
|
||||||
)
|
|
||||||
.await);
|
|
||||||
}
|
|
||||||
|
|
||||||
let endpoint = match Endpoint::get_by_id(*database.lock().await, endpoint_id)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
{
|
|
||||||
Some(x) => x,
|
|
||||||
None => {
|
|
||||||
error!("No endpoint found by id {endpoint_id} from {address}",);
|
|
||||||
|
|
||||||
return Ok(custom_resp(
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
"No endpoint by that ID.".to_string(),
|
|
||||||
)
|
|
||||||
.await);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let hosts = json_to_vec(body["hosts"].clone()).await.unwrap();
|
|
||||||
|
|
||||||
endpoint.host(*database.lock().await, hosts).await.unwrap();
|
|
||||||
|
|
||||||
Ok(custom_resp(StatusCode::OK, "Success".to_string()).await)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RegisterEndpoint {}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Route<ApiMatcher> for RegisterEndpoint {
|
|
||||||
fn matcher(&self, _: &ApiMatcher, req: &hyper::Request<hyper::body::Incoming>) -> bool {
|
|
||||||
req.uri().path() == "/register" && req.method() == Method::POST
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn call(
|
|
||||||
&self,
|
|
||||||
m: &ApiMatcher,
|
|
||||||
_: Parts,
|
|
||||||
) -> Result<crate::server::GeneralResponse, hyper::Error> {
|
|
||||||
let address = m._address.unwrap();
|
|
||||||
let database = m.database.clone();
|
|
||||||
let body = m.body.clone().unwrap();
|
|
||||||
|
|
||||||
let mut endpoint = Endpoint::new(
|
|
||||||
None,
|
|
||||||
address,
|
|
||||||
body["port"].as_u16().unwrap_or(8080),
|
|
||||||
body["callback"].as_str().unwrap_or("/").to_string(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if !body["hosts"].is_array() {
|
|
||||||
error!("Hosts parameter is not an array.",);
|
|
||||||
|
|
||||||
return Ok(custom_resp(
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
"Hosts parameter is not an array.".to_string(),
|
|
||||||
)
|
|
||||||
.await);
|
|
||||||
};
|
|
||||||
|
|
||||||
let hosts = json_to_vec(body["hosts"].clone()).await.unwrap();
|
|
||||||
|
|
||||||
endpoint
|
|
||||||
.register(*database.lock().await, hosts)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let endpoint_id = endpoint.id.unwrap().to_string();
|
|
||||||
|
|
||||||
let response = custom_resp(StatusCode::OK, endpoint_id).await;
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RemoveHost {}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Route<ApiMatcher> for RemoveHost {
|
|
||||||
fn matcher(&self, _: &ApiMatcher, req: &hyper::Request<hyper::body::Incoming>) -> bool {
|
|
||||||
req.uri().path().starts_with("/endpoint/") && req.method() == Method::DELETE
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn call(
|
|
||||||
&self,
|
|
||||||
m: &ApiMatcher,
|
|
||||||
parts: Parts,
|
|
||||||
) -> Result<crate::server::GeneralResponse, hyper::Error> {
|
|
||||||
let database = m.database.clone();
|
|
||||||
let address = m._address.unwrap();
|
|
||||||
|
|
||||||
let endpoint_id: u32 = parts.uri.path().replace("/endpoint/", "").parse().unwrap();
|
|
||||||
|
|
||||||
let endpoint = match Endpoint::get_by_id(*database.lock().await, endpoint_id)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
{
|
|
||||||
Some(x) => x,
|
|
||||||
None => {
|
|
||||||
error!("No endpoint found by id {endpoint_id} from {address}",);
|
|
||||||
|
|
||||||
return Ok(custom_resp(
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
"No endpoint by that ID.".to_string(),
|
|
||||||
)
|
|
||||||
.await);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
endpoint.delete(*database.lock().await).await.unwrap();
|
|
||||||
|
|
||||||
Ok(custom_resp(StatusCode::OK, "Success".to_string()).await)
|
|
||||||
}
|
|
||||||
}
|
|
105
src/server.rs
105
src/server.rs
|
@ -1,105 +0,0 @@
|
||||||
use std::{any::type_name_of_val, error::Error};
|
|
||||||
|
|
||||||
use http_body_util::{Either, Full};
|
|
||||||
use hyper::{
|
|
||||||
Request, Response, StatusCode,
|
|
||||||
body::{Body, Bytes, Incoming},
|
|
||||||
server::conn::http1,
|
|
||||||
service::{HttpService, Service},
|
|
||||||
};
|
|
||||||
use hyper_util::rt::TokioIo;
|
|
||||||
use json::JsonValue;
|
|
||||||
use log::{error, info};
|
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
|
||||||
|
|
||||||
pub type GeneralResponse = Response<GeneralBody>;
|
|
||||||
pub type GeneralBody = Either<Incoming, Full<Bytes>>;
|
|
||||||
|
|
||||||
pub fn to_general_response(res: Response<Incoming>) -> GeneralResponse {
|
|
||||||
let (parts, body) = res.into_parts();
|
|
||||||
Response::from_parts(parts, GeneralBody::Left(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Server<S> {
|
|
||||||
listener: TcpListener,
|
|
||||||
service: S,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait TcpIntercept {
|
|
||||||
fn stream(&mut self, stream: &TcpStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn default_response() -> GeneralResponse {
|
|
||||||
Response::builder()
|
|
||||||
.status(404)
|
|
||||||
.body(GeneralBody::Right(Full::from(Bytes::from(
|
|
||||||
"That route doesn't exist.",
|
|
||||||
))))
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn custom_resp(e: StatusCode, m: String) -> GeneralResponse {
|
|
||||||
Response::builder()
|
|
||||||
.status(e)
|
|
||||||
.body(GeneralBody::Right(Full::from(Bytes::from(m))))
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn json_to_vec(v: JsonValue) -> Option<Vec<String>> {
|
|
||||||
if let JsonValue::Array(arr) = v {
|
|
||||||
Some(
|
|
||||||
arr.into_iter()
|
|
||||||
.map(|val| val.as_str().unwrap().to_string())
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> Server<S>
|
|
||||||
where
|
|
||||||
S: TcpIntercept,
|
|
||||||
S: Service<Request<Incoming>> + Clone + Send + 'static,
|
|
||||||
S: HttpService<Incoming> + Clone + Send,
|
|
||||||
<S::ResBody as Body>::Error: Into<Box<dyn Error + Send + Sync>>,
|
|
||||||
<S::ResBody as Body>::Data: Send,
|
|
||||||
S::ResBody: Send,
|
|
||||||
<S as HttpService<Incoming>>::Future: Send,
|
|
||||||
{
|
|
||||||
pub async fn handle(&self) {
|
|
||||||
info!(
|
|
||||||
"Server started at http://{} for service: {}",
|
|
||||||
self.listener.local_addr().unwrap(),
|
|
||||||
type_name_of_val(&self.service)
|
|
||||||
);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let (stream, _) = self.listener.accept().await.unwrap();
|
|
||||||
|
|
||||||
let mut svc_clone = self.service.clone();
|
|
||||||
svc_clone.stream(&stream);
|
|
||||||
|
|
||||||
let io = TokioIo::new(stream);
|
|
||||||
|
|
||||||
tokio::task::spawn(async move {
|
|
||||||
if let Err(err) = http1::Builder::new()
|
|
||||||
.writev(false)
|
|
||||||
.serve_connection(io, svc_clone)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
error!("Error while trying to serve connection: {err}")
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn new(service: S, a: (String, u16)) -> Result<Self, Box<dyn Error>> {
|
|
||||||
Ok(Self {
|
|
||||||
listener: TcpListener::bind(&a).await?,
|
|
||||||
service,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
*/
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod controller;
|
|
||||||
pub mod matcher;
|
|
||||||
pub mod proxy;
|
|
|
@ -1,72 +0,0 @@
|
||||||
use std::{pin::Pin, sync::Arc};
|
|
||||||
|
|
||||||
use hyper::{Request, StatusCode, body::Incoming, service::Service};
|
|
||||||
use log::error;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
db::{BoxyDatabase, Endpoint},
|
|
||||||
server::{GeneralResponse, TcpIntercept, custom_resp},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::proxy::ProxyService;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ControllerService {
|
|
||||||
pub database: Arc<Mutex<&'static mut BoxyDatabase>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TcpIntercept for ControllerService {
|
|
||||||
fn stream(&mut self, _: &tokio::net::TcpStream) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Service<Request<Incoming>> for ControllerService {
|
|
||||||
type Response = GeneralResponse;
|
|
||||||
type Error = hyper::Error;
|
|
||||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
|
||||||
|
|
||||||
fn call(&self, req: Request<Incoming>) -> Self::Future {
|
|
||||||
let database = self.database.clone();
|
|
||||||
Box::pin(async move {
|
|
||||||
let hostname = match req.headers().get(hyper::header::HOST) {
|
|
||||||
Some(x) => x,
|
|
||||||
None => {
|
|
||||||
error!("No host header given.");
|
|
||||||
|
|
||||||
return Ok(custom_resp(
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
"No host header given.".to_string(),
|
|
||||||
)
|
|
||||||
.await);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let endpoints = Endpoint::get_by_hostname(*database.lock().await, hostname.clone())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let endpoint = match endpoints.first() {
|
|
||||||
Some(x) => x,
|
|
||||||
None => {
|
|
||||||
error!("No endpoint found for request.");
|
|
||||||
|
|
||||||
return Ok(custom_resp(
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
"No endpoint found for host.".to_string(),
|
|
||||||
)
|
|
||||||
.await);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let proxy = ProxyService {
|
|
||||||
address: format!("{}:{}", endpoint.address.clone(), endpoint.port),
|
|
||||||
hostname,
|
|
||||||
};
|
|
||||||
|
|
||||||
proxy.call(req).await
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,120 +0,0 @@
|
||||||
use std::{pin::Pin, sync::Arc};
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use http::request::Parts;
|
|
||||||
use hyper::{Request, StatusCode, body::Incoming, service::Service};
|
|
||||||
use tokio::net::TcpStream;
|
|
||||||
|
|
||||||
use crate::server::{GeneralResponse, TcpIntercept, custom_resp, default_response};
|
|
||||||
|
|
||||||
// The routes itself
|
|
||||||
#[async_trait]
|
|
||||||
pub trait Route<T>
|
|
||||||
where
|
|
||||||
T: Matcher,
|
|
||||||
{
|
|
||||||
fn matcher(&self, m: &T, req: &Request<Incoming>) -> bool;
|
|
||||||
async fn call(&self, m: &T, parts: Parts) -> Result<GeneralResponse, hyper::Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Matcher, essentially just a router that contains routes and some other features
|
|
||||||
#[async_trait]
|
|
||||||
pub trait Matcher: Clone + Send + Sync + 'static {
|
|
||||||
// Essentially a kind of "middleware", a universal matcher. If it doesn't match, it won't
|
|
||||||
// route.
|
|
||||||
async fn unimatch(
|
|
||||||
&mut self,
|
|
||||||
req: &Request<Incoming>,
|
|
||||||
) -> (bool, Option<Result<GeneralResponse, hyper::Error>>);
|
|
||||||
|
|
||||||
// Return list of routes associated with self matcher
|
|
||||||
fn retrieve(&self) -> Vec<Arc<dyn Route<Self> + Sync + Send>>;
|
|
||||||
|
|
||||||
// Wrap self into matcher service
|
|
||||||
fn service(self) -> MatcherService<Self> {
|
|
||||||
MatcherService::new(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do something with TCP stream
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
fn stream(&mut self, stream: &TcpStream) {}
|
|
||||||
|
|
||||||
// Body parser - made universal for api server cause lazy
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
async fn body(&mut self, body: Incoming) -> Option<Result<GeneralResponse, hyper::Error>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrapper service, wraps matcher into a service
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct MatcherService<T>
|
|
||||||
where
|
|
||||||
T: Matcher,
|
|
||||||
{
|
|
||||||
inner: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> MatcherService<T>
|
|
||||||
where
|
|
||||||
T: Matcher,
|
|
||||||
{
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Service<Request<Incoming>> for MatcherService<T>
|
|
||||||
where
|
|
||||||
T: Matcher,
|
|
||||||
{
|
|
||||||
type Response = GeneralResponse;
|
|
||||||
type Error = hyper::Error;
|
|
||||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
|
||||||
|
|
||||||
fn call(&self, req: Request<Incoming>) -> Self::Future {
|
|
||||||
let mut matcher = self.inner.clone();
|
|
||||||
|
|
||||||
Box::pin(async move {
|
|
||||||
let unimatched = matcher.unimatch(&req).await;
|
|
||||||
|
|
||||||
if !unimatched.0 {
|
|
||||||
match unimatched.1 {
|
|
||||||
Some(x) => {
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
return Ok(custom_resp(
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
"Could not match route".to_string(),
|
|
||||||
)
|
|
||||||
.await);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for r in matcher.retrieve() {
|
|
||||||
if r.matcher(&matcher, &req) {
|
|
||||||
let (parts, body) = req.into_parts();
|
|
||||||
|
|
||||||
if let Some(resp) = matcher.body(body).await {
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.call(&matcher, parts).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(default_response().await)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> TcpIntercept for MatcherService<T>
|
|
||||||
where
|
|
||||||
T: Matcher,
|
|
||||||
{
|
|
||||||
fn stream(&mut self, stream: &TcpStream) {
|
|
||||||
self.inner.stream(stream);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
use std::pin::Pin;
|
|
||||||
|
|
||||||
use hyper::{Request, StatusCode, body::Incoming, service::Service};
|
|
||||||
use hyper_util::rt::TokioIo;
|
|
||||||
use log::error;
|
|
||||||
use tokio::net::TcpStream;
|
|
||||||
|
|
||||||
use crate::server::{GeneralResponse, custom_resp, to_general_response};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ProxyService {
|
|
||||||
pub address: String,
|
|
||||||
pub hostname: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Service<Request<Incoming>> for ProxyService {
|
|
||||||
type Response = GeneralResponse;
|
|
||||||
type Error = hyper::Error;
|
|
||||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
|
||||||
|
|
||||||
fn call(&self, incoming: Request<Incoming>) -> Self::Future {
|
|
||||||
let address = self.address.clone();
|
|
||||||
let hostname = self.hostname.clone();
|
|
||||||
Box::pin(async move {
|
|
||||||
let stream = match TcpStream::connect(address).await {
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Could not open connection to endpoint: {e}");
|
|
||||||
|
|
||||||
return Ok(custom_resp(
|
|
||||||
StatusCode::BAD_GATEWAY,
|
|
||||||
"Unable to open connection to endpoint.".to_string(),
|
|
||||||
)
|
|
||||||
.await);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let io = TokioIo::new(stream);
|
|
||||||
|
|
||||||
let (mut sender, conn) = hyper::client::conn::http1::Builder::new()
|
|
||||||
.handshake(io)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(err) = conn.await {
|
|
||||||
error!("Could not open connection to backend: {err}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let (mut parts, body_stream) = incoming.into_parts();
|
|
||||||
|
|
||||||
parts
|
|
||||||
.headers
|
|
||||||
.insert(hyper::header::HOST, hostname.parse().unwrap());
|
|
||||||
|
|
||||||
let req = Request::from_parts(parts, body_stream);
|
|
||||||
|
|
||||||
Ok(to_general_response(sender.send_request(req).await.unwrap()))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
0
test
0
test
Loading…
Add table
Add a link
Reference in a new issue