@nmtjs/proxy 0.15.0-beta.2 → 0.15.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +2 -0
- package/Cargo.toml +5 -3
- package/package.json +10 -3
- package/src/errors.rs +28 -0
- package/src/lb.rs +45 -0
- package/src/lib.rs +5 -3
- package/src/options.rs +178 -0
- package/src/proxy.rs +642 -102
- package/src/router.rs +452 -345
- package/src/server.rs +80 -0
- package/src/config.rs +0 -199
package/Cargo.lock
CHANGED
|
@@ -1231,6 +1231,7 @@ dependencies = [
|
|
|
1231
1231
|
name = "neemata-proxy"
|
|
1232
1232
|
version = "0.1.0"
|
|
1233
1233
|
dependencies = [
|
|
1234
|
+
"arc-swap",
|
|
1234
1235
|
"async-trait",
|
|
1235
1236
|
"env_logger",
|
|
1236
1237
|
"http",
|
|
@@ -1241,6 +1242,7 @@ dependencies = [
|
|
|
1241
1242
|
"pingora",
|
|
1242
1243
|
"pingora-load-balancing",
|
|
1243
1244
|
"tokio",
|
|
1245
|
+
"tokio-util",
|
|
1244
1246
|
"url",
|
|
1245
1247
|
]
|
|
1246
1248
|
|
package/Cargo.toml
CHANGED
|
@@ -7,13 +7,15 @@ edition = "2024"
|
|
|
7
7
|
crate-type = ["cdylib"]
|
|
8
8
|
|
|
9
9
|
[dependencies]
|
|
10
|
-
napi = { version = "3", features = ["async"] }
|
|
10
|
+
napi = { version = "3", features = ["async", "tokio_rt", "napi4"] }
|
|
11
11
|
napi-derive = "3.0.0"
|
|
12
12
|
async-trait = "0.1"
|
|
13
|
-
pingora = { version = "0.6", features = ["lb"] }
|
|
13
|
+
pingora = { version = "0.6", features = ["lb", "proxy"] }
|
|
14
14
|
pingora-load-balancing = "0.6"
|
|
15
15
|
http = "1.0"
|
|
16
|
-
tokio = { version = "1.48.0", features = ["sync"] }
|
|
16
|
+
tokio = { version = "1.48.0", features = ["macros", "net", "rt-multi-thread", "sync", "time"] }
|
|
17
|
+
tokio-util = "0.7"
|
|
18
|
+
arc-swap = "1.7"
|
|
17
19
|
url = "2.5.7"
|
|
18
20
|
log = "0.4.28"
|
|
19
21
|
env_logger = "0.11.8"
|
package/package.json
CHANGED
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@napi-rs/cli": "^3.4.1"
|
|
12
12
|
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/node": "^24.0.0",
|
|
15
|
+
"typescript": "^5.9.0",
|
|
16
|
+
"vitest": "^3.2.0",
|
|
17
|
+
"@types/ws": "^8.18.1",
|
|
18
|
+
"ws": "^8.18.3"
|
|
19
|
+
},
|
|
13
20
|
"files": [
|
|
14
21
|
"src",
|
|
15
22
|
"build.rs",
|
|
@@ -18,13 +25,13 @@
|
|
|
18
25
|
"LICENSE.md",
|
|
19
26
|
"README.md"
|
|
20
27
|
],
|
|
21
|
-
"version": "0.15.0-beta.
|
|
28
|
+
"version": "0.15.0-beta.20",
|
|
22
29
|
"scripts": {
|
|
23
30
|
"postinstall": "$npm_execpath build",
|
|
24
|
-
"clean-build": "true",
|
|
25
31
|
"fmt": "cargo fmt",
|
|
26
32
|
"lint": "cargo clippy",
|
|
27
33
|
"build": "$npm_execpath build:debug --release",
|
|
28
|
-
"build:debug": "napi build --esm --js index.js --platform --output-dir ./dist"
|
|
34
|
+
"build:debug": "napi build --esm --js index.js --platform --output-dir ./dist",
|
|
35
|
+
"test": "cargo test && vitest run"
|
|
29
36
|
}
|
|
30
37
|
}
|
package/src/errors.rs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
use napi::{Env, Error, Result, Status};
|
|
2
|
+
|
|
3
|
+
pub mod codes {
|
|
4
|
+
pub const INVALID_PROXY_OPTIONS: &str = "InvalidProxyOptions";
|
|
5
|
+
pub const INVALID_APPLICATION_OPTIONS: &str = "InvalidApplicationOptions";
|
|
6
|
+
pub const UNKNOWN_APPLICATION: &str = "UnknownApplication";
|
|
7
|
+
pub const ALREADY_STARTED: &str = "AlreadyStarted";
|
|
8
|
+
pub const LISTEN_BIND_FAILED: &str = "ListenBindFailed";
|
|
9
|
+
pub const UNSUPPORTED_UPSTREAM_TYPE: &str = "UnsupportedUpstreamType";
|
|
10
|
+
pub const UPSTREAM_ALREADY_EXISTS: &str = "UpstreamAlreadyExists";
|
|
11
|
+
pub const UPSTREAM_NOT_FOUND: &str = "UpstreamNotFound";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
pub fn type_error(env: &Env, code: &str, msg: impl AsRef<str>) -> Error {
|
|
15
|
+
let msg = msg.as_ref();
|
|
16
|
+
let _ = env.throw_type_error(msg, Some(code));
|
|
17
|
+
Error::new(Status::InvalidArg, msg.to_owned())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub fn generic_error(env: &Env, code: &str, msg: impl AsRef<str>) -> Error {
|
|
21
|
+
let msg = msg.as_ref();
|
|
22
|
+
let _ = env.throw_error(msg, Some(code));
|
|
23
|
+
Error::new(Status::GenericFailure, msg.to_owned())
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub fn throw_type_error<T>(env: &Env, code: &str, msg: impl AsRef<str>) -> Result<T> {
|
|
27
|
+
Err(type_error(env, code, msg))
|
|
28
|
+
}
|
package/src/lb.rs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
use std::{net::SocketAddr, sync::Arc, time::Duration};
|
|
2
|
+
|
|
3
|
+
use pingora::lb::{LoadBalancer, selection::RoundRobin};
|
|
4
|
+
use pingora::server::ShutdownWatch;
|
|
5
|
+
use pingora::services::background::BackgroundService;
|
|
6
|
+
use tokio::sync::watch;
|
|
7
|
+
|
|
8
|
+
use crate::server::ServiceHandle;
|
|
9
|
+
|
|
10
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
11
|
+
#[allow(dead_code)]
|
|
12
|
+
pub enum TransportKind {
|
|
13
|
+
Http1,
|
|
14
|
+
Http2,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[allow(dead_code)]
|
|
18
|
+
pub fn build_round_robin_lb(
|
|
19
|
+
addrs: Vec<SocketAddr>,
|
|
20
|
+
health_check_frequency: Option<Duration>,
|
|
21
|
+
) -> std::io::Result<Arc<LoadBalancer<RoundRobin>>> {
|
|
22
|
+
let mut lb = LoadBalancer::<RoundRobin>::try_from_iter(addrs)?;
|
|
23
|
+
lb.health_check_frequency = health_check_frequency;
|
|
24
|
+
Ok(Arc::new(lb))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[allow(dead_code)]
|
|
28
|
+
pub fn spawn_lb_health_task(lb: Arc<LoadBalancer<RoundRobin>>) -> ServiceHandle {
|
|
29
|
+
let cancel = tokio_util::sync::CancellationToken::new();
|
|
30
|
+
let cancel_child = cancel.clone();
|
|
31
|
+
|
|
32
|
+
let (shutdown_tx, shutdown_rx): (watch::Sender<bool>, ShutdownWatch) = watch::channel(false);
|
|
33
|
+
|
|
34
|
+
let task = tokio::spawn(async move {
|
|
35
|
+
let start_fut = lb.start(shutdown_rx);
|
|
36
|
+
tokio::select! {
|
|
37
|
+
_ = start_fut => {}
|
|
38
|
+
_ = cancel_child.cancelled() => {
|
|
39
|
+
let _ = shutdown_tx.send(true);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
ServiceHandle { cancel, task }
|
|
45
|
+
}
|
package/src/lib.rs
CHANGED
package/src/options.rs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
use crate::errors;
|
|
2
|
+
use napi::bindgen_prelude::*;
|
|
3
|
+
use napi_derive::napi;
|
|
4
|
+
|
|
5
|
+
#[napi(object)]
|
|
6
|
+
pub struct ProxyOptions {
|
|
7
|
+
pub listen: String,
|
|
8
|
+
pub tls: Option<ListenerTlsOptions>,
|
|
9
|
+
pub applications: Vec<ApplicationOptions>,
|
|
10
|
+
/// Health check interval in milliseconds. Defaults to 5000ms (5 seconds).
|
|
11
|
+
pub health_check_interval_ms: Option<u32>,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#[napi(object)]
|
|
15
|
+
pub struct ListenerTlsOptions {
|
|
16
|
+
pub cert_path: String,
|
|
17
|
+
pub key_path: String,
|
|
18
|
+
pub enable_h2: Option<bool>,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[napi(object)]
|
|
22
|
+
pub struct ApplicationOptions {
|
|
23
|
+
pub name: String,
|
|
24
|
+
pub routing: RoutingOptions,
|
|
25
|
+
pub sni: Option<String>,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#[napi(object)]
|
|
29
|
+
pub struct RoutingOptions {
|
|
30
|
+
#[napi(ts_type = " 'subdomain' | 'path' ")]
|
|
31
|
+
pub r#type: Option<String>,
|
|
32
|
+
pub name: Option<String>,
|
|
33
|
+
pub default: Option<bool>,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[derive(Debug, Clone)]
|
|
37
|
+
pub struct ProxyOptionsParsed {
|
|
38
|
+
pub listen: String,
|
|
39
|
+
pub tls: Option<ListenerTlsOptionsParsed>,
|
|
40
|
+
pub applications: Vec<ApplicationOptionsParsed>,
|
|
41
|
+
/// Health check interval in milliseconds.
|
|
42
|
+
pub health_check_interval_ms: u32,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[derive(Debug, Clone)]
|
|
46
|
+
pub struct ListenerTlsOptionsParsed {
|
|
47
|
+
pub cert_path: String,
|
|
48
|
+
pub key_path: String,
|
|
49
|
+
pub enable_h2: bool,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[derive(Debug, Clone)]
|
|
53
|
+
pub struct ApplicationOptionsParsed {
|
|
54
|
+
pub name: String,
|
|
55
|
+
pub routing: ApplicationRoutingParsed,
|
|
56
|
+
pub sni: Option<String>,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#[derive(Debug, Clone)]
|
|
60
|
+
pub enum ApplicationRoutingParsed {
|
|
61
|
+
Subdomain { name: String },
|
|
62
|
+
Path { name: String },
|
|
63
|
+
Default,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pub fn parse_proxy_options(env: &Env, options: ProxyOptions) -> Result<ProxyOptionsParsed> {
|
|
67
|
+
let listen = options.listen;
|
|
68
|
+
|
|
69
|
+
let tls = options.tls.map(|tls| ListenerTlsOptionsParsed {
|
|
70
|
+
cert_path: tls.cert_path,
|
|
71
|
+
key_path: tls.key_path,
|
|
72
|
+
enable_h2: tls.enable_h2.unwrap_or(false),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
let mut applications = Vec::with_capacity(options.applications.len());
|
|
76
|
+
for app in options.applications {
|
|
77
|
+
applications.push(parse_application_options(env, app)?);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
validate_applications(env, &applications)?;
|
|
81
|
+
|
|
82
|
+
Ok(ProxyOptionsParsed {
|
|
83
|
+
listen,
|
|
84
|
+
tls,
|
|
85
|
+
applications,
|
|
86
|
+
health_check_interval_ms: options.health_check_interval_ms.unwrap_or(5000),
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn parse_application_options(
|
|
91
|
+
env: &Env,
|
|
92
|
+
app: ApplicationOptions,
|
|
93
|
+
) -> Result<ApplicationOptionsParsed> {
|
|
94
|
+
let name = app.name;
|
|
95
|
+
let sni = app.sni;
|
|
96
|
+
let routing = parse_routing(env, app.routing)?;
|
|
97
|
+
Ok(ApplicationOptionsParsed { name, routing, sni })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn parse_routing(env: &Env, routing: RoutingOptions) -> Result<ApplicationRoutingParsed> {
|
|
101
|
+
if routing.default.unwrap_or(false) {
|
|
102
|
+
return Ok(ApplicationRoutingParsed::Default);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let Some(routing_type) = routing.r#type else {
|
|
106
|
+
return errors::throw_type_error(
|
|
107
|
+
env,
|
|
108
|
+
errors::codes::INVALID_APPLICATION_OPTIONS,
|
|
109
|
+
"ApplicationOptions.routing.type must be set unless routing.default=true",
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
let Some(routing_name) = routing.name else {
|
|
113
|
+
return errors::throw_type_error(
|
|
114
|
+
env,
|
|
115
|
+
errors::codes::INVALID_APPLICATION_OPTIONS,
|
|
116
|
+
"ApplicationOptions.routing.name must be set unless routing.default=true",
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
match routing_type.as_str() {
|
|
121
|
+
"subdomain" => Ok(ApplicationRoutingParsed::Subdomain { name: routing_name }),
|
|
122
|
+
"path" => Ok(ApplicationRoutingParsed::Path { name: routing_name }),
|
|
123
|
+
other => errors::throw_type_error(
|
|
124
|
+
env,
|
|
125
|
+
errors::codes::INVALID_APPLICATION_OPTIONS,
|
|
126
|
+
format!(
|
|
127
|
+
"Unknown routing.type '{other}'. Expected 'subdomain' or 'path', or use routing.default=true"
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fn validate_applications(env: &Env, applications: &[ApplicationOptionsParsed]) -> Result<()> {
|
|
134
|
+
let mut default_count = 0usize;
|
|
135
|
+
|
|
136
|
+
for app in applications {
|
|
137
|
+
match &app.routing {
|
|
138
|
+
ApplicationRoutingParsed::Default => {
|
|
139
|
+
default_count += 1;
|
|
140
|
+
}
|
|
141
|
+
ApplicationRoutingParsed::Subdomain { name } => {
|
|
142
|
+
if name.is_empty() {
|
|
143
|
+
return errors::throw_type_error(
|
|
144
|
+
env,
|
|
145
|
+
errors::codes::INVALID_APPLICATION_OPTIONS,
|
|
146
|
+
"subdomain routing name must be non-empty",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
ApplicationRoutingParsed::Path { name } => {
|
|
151
|
+
if name.is_empty() {
|
|
152
|
+
return errors::throw_type_error(
|
|
153
|
+
env,
|
|
154
|
+
errors::codes::INVALID_APPLICATION_OPTIONS,
|
|
155
|
+
"path routing name must be non-empty",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if name.contains('/') {
|
|
159
|
+
return errors::throw_type_error(
|
|
160
|
+
env,
|
|
161
|
+
errors::codes::INVALID_APPLICATION_OPTIONS,
|
|
162
|
+
"path routing name must not contain '/'",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if default_count > 1 {
|
|
170
|
+
return errors::throw_type_error(
|
|
171
|
+
env,
|
|
172
|
+
errors::codes::INVALID_APPLICATION_OPTIONS,
|
|
173
|
+
"At most one application may have routing.default=true",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
Ok(())
|
|
178
|
+
}
|