@assetsart/nylon-mesh 1.0.1

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.
Files changed (49) hide show
  1. package/.github/workflows/release.yml +98 -0
  2. package/Cargo.lock +2965 -0
  3. package/Cargo.toml +33 -0
  4. package/README.md +104 -0
  5. package/bin/nylon-mesh.js +213 -0
  6. package/bun.lock +360 -0
  7. package/docs/content/docs/caching.mdx +85 -0
  8. package/docs/content/docs/configuration.mdx +115 -0
  9. package/docs/content/docs/index.mdx +58 -0
  10. package/docs/content/docs/load-balancing.mdx +69 -0
  11. package/docs/content/docs/meta.json +9 -0
  12. package/docs/next.config.mjs +11 -0
  13. package/docs/package-lock.json +6099 -0
  14. package/docs/package.json +32 -0
  15. package/docs/postcss.config.mjs +7 -0
  16. package/docs/source.config.ts +23 -0
  17. package/docs/src/app/(home)/layout.tsx +6 -0
  18. package/docs/src/app/(home)/page.tsx +125 -0
  19. package/docs/src/app/api/search/route.ts +9 -0
  20. package/docs/src/app/docs/[[...slug]]/page.tsx +46 -0
  21. package/docs/src/app/docs/layout.tsx +11 -0
  22. package/docs/src/app/global.css +7 -0
  23. package/docs/src/app/layout.tsx +31 -0
  24. package/docs/src/app/llms-full.txt/route.ts +10 -0
  25. package/docs/src/app/llms.txt/route.ts +13 -0
  26. package/docs/src/app/og/docs/[...slug]/route.tsx +27 -0
  27. package/docs/src/components/ai/page-actions.tsx +240 -0
  28. package/docs/src/components/architecture-diagram.tsx +88 -0
  29. package/docs/src/components/benchmark.tsx +129 -0
  30. package/docs/src/components/configuration.tsx +107 -0
  31. package/docs/src/components/copy-button.tsx +29 -0
  32. package/docs/src/components/footer.tsx +37 -0
  33. package/docs/src/components/framework-logos.tsx +35 -0
  34. package/docs/src/lib/cn.ts +1 -0
  35. package/docs/src/lib/layout.shared.tsx +23 -0
  36. package/docs/src/lib/source.ts +27 -0
  37. package/docs/src/mdx-components.tsx +9 -0
  38. package/docs/tsconfig.json +46 -0
  39. package/nylon-mesh.yaml +41 -0
  40. package/package.json +23 -0
  41. package/scripts/publish.mjs +18 -0
  42. package/scripts/release.mjs +52 -0
  43. package/src/config.rs +91 -0
  44. package/src/main.rs +214 -0
  45. package/src/proxy/cache.rs +304 -0
  46. package/src/proxy/handlers.rs +76 -0
  47. package/src/proxy/load_balancer.rs +23 -0
  48. package/src/proxy/mod.rs +232 -0
  49. package/src/tls_accept.rs +119 -0
@@ -0,0 +1,76 @@
1
+ use bytes::Bytes;
2
+ use http::StatusCode;
3
+ use pingora::Result;
4
+ use pingora::http::ResponseHeader;
5
+ use pingora_proxy::Session;
6
+
7
+ use super::MeshProxy;
8
+
9
+ impl MeshProxy {
10
+ pub async fn serve_probe_response(
11
+ session: &mut Session,
12
+ status: StatusCode,
13
+ msg: &'static str,
14
+ ) -> Result<()> {
15
+ let mut header = ResponseHeader::build(status, None).unwrap();
16
+ let _ = header.insert_header("Content-Length", msg.len().to_string());
17
+ session
18
+ .write_response_header(Box::new(header), true)
19
+ .await?;
20
+ session
21
+ .write_response_body(Some(Bytes::from(msg)), true)
22
+ .await?;
23
+ Ok(())
24
+ }
25
+
26
+ pub async fn handle_probes(&self, session: &mut Session, uri: &str) -> Result<Option<bool>> {
27
+ if let Some(liveness_path) = &self.config.liveness_path {
28
+ if uri == liveness_path {
29
+ Self::serve_probe_response(session, StatusCode::OK, "OK").await?;
30
+ return Ok(Some(true));
31
+ }
32
+ }
33
+
34
+ if let Some(readiness_path) = &self.config.readiness_path {
35
+ if uri == readiness_path {
36
+ if crate::is_shutting_down() {
37
+ Self::serve_probe_response(
38
+ session,
39
+ StatusCode::SERVICE_UNAVAILABLE,
40
+ "Service is shutting down",
41
+ )
42
+ .await?;
43
+ } else {
44
+ Self::serve_probe_response(session, StatusCode::OK, "OK").await?;
45
+ }
46
+ return Ok(Some(true));
47
+ }
48
+ }
49
+
50
+ Ok(None)
51
+ }
52
+
53
+ pub fn should_bypass_cache(&self, method: &str, uri: &str) -> bool {
54
+ if method != "GET" {
55
+ return true;
56
+ }
57
+
58
+ if let Some(bypass) = &self.config.bypass {
59
+ if let Some(paths) = &bypass.paths {
60
+ for p in paths {
61
+ if uri.starts_with(p) {
62
+ return true;
63
+ }
64
+ }
65
+ }
66
+ if let Some(exts) = &bypass.extensions {
67
+ for ext in exts {
68
+ if uri.ends_with(ext) {
69
+ return true;
70
+ }
71
+ }
72
+ }
73
+ }
74
+ false
75
+ }
76
+ }
@@ -0,0 +1,23 @@
1
+ use pingora_load_balancing::{
2
+ LoadBalancer,
3
+ selection::{Random, RoundRobin},
4
+ };
5
+ use std::sync::Arc;
6
+
7
+ pub enum MeshLoadBalancer {
8
+ RoundRobin(Arc<LoadBalancer<RoundRobin>>),
9
+ Random(Arc<LoadBalancer<Random>>),
10
+ }
11
+
12
+ impl MeshLoadBalancer {
13
+ pub fn select(
14
+ &self,
15
+ key: &[u8],
16
+ max_iterations: usize,
17
+ ) -> Option<pingora_load_balancing::Backend> {
18
+ match self {
19
+ Self::RoundRobin(lb) => lb.select(key, max_iterations),
20
+ Self::Random(lb) => lb.select(key, max_iterations),
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,232 @@
1
+ pub mod cache;
2
+ pub mod handlers;
3
+ pub mod load_balancer;
4
+
5
+ pub use load_balancer::MeshLoadBalancer;
6
+
7
+ use async_trait::async_trait;
8
+ use bytes::Bytes;
9
+ use moka::future::Cache;
10
+ use pingora::Result;
11
+ use pingora::http::ResponseHeader;
12
+ use pingora::upstreams::peer::HttpPeer;
13
+ use pingora_proxy::{ProxyHttp, Session};
14
+ use std::sync::Arc;
15
+ use std::time::Duration;
16
+
17
+ use crate::config::Config;
18
+
19
+ pub struct MeshProxy {
20
+ pub config: Arc<Config>,
21
+ pub load_balancer: Arc<MeshLoadBalancer>,
22
+ pub tier1_cache: Cache<String, (ResponseHeader, Bytes)>,
23
+ pub encoding_hits: Arc<std::collections::HashMap<&'static str, std::sync::atomic::AtomicU64>>,
24
+ }
25
+
26
+ pub struct ProxyCtx {
27
+ pub should_cache: bool,
28
+ pub cache_key: String,
29
+ pub host: String,
30
+ pub response_body: Vec<u8>,
31
+ pub response_header: Option<ResponseHeader>,
32
+ }
33
+
34
+ #[async_trait]
35
+ impl ProxyHttp for MeshProxy {
36
+ type CTX = ProxyCtx;
37
+
38
+ fn new_ctx(&self) -> Self::CTX {
39
+ ProxyCtx {
40
+ should_cache: false,
41
+ cache_key: String::new(),
42
+ host: String::new(),
43
+ response_body: Vec::new(),
44
+ response_header: None,
45
+ }
46
+ }
47
+
48
+ async fn upstream_peer(
49
+ &self,
50
+ _session: &mut Session,
51
+ _ctx: &mut Self::CTX,
52
+ ) -> Result<Box<HttpPeer>> {
53
+ let upstream = self
54
+ .load_balancer
55
+ .select(b"", 256) // Use empty hash for round robin
56
+ .ok_or_else(|| {
57
+ pingora::Error::explain(
58
+ pingora::ErrorType::HTTPStatus(502),
59
+ "No upstream available",
60
+ )
61
+ })?;
62
+
63
+ let peer = HttpPeer::new(upstream.clone(), false, String::new());
64
+ Ok(Box::new(peer))
65
+ }
66
+
67
+ async fn request_filter(&self, session: &mut Session, ctx: &mut Self::CTX) -> Result<bool> {
68
+ let uri = session.req_header().uri.path().to_string();
69
+
70
+ if let Some(handled) = self.handle_probes(session, &uri).await? {
71
+ return Ok(handled);
72
+ }
73
+
74
+ let method = session.req_header().method.as_str().to_string();
75
+
76
+ if self.should_bypass_cache(&method, &uri) {
77
+ return Ok(false);
78
+ }
79
+
80
+ let host = session
81
+ .req_header()
82
+ .headers
83
+ .get("Host")
84
+ .map(|v| v.to_str().unwrap_or("localhost"))
85
+ .unwrap_or("localhost")
86
+ .to_string();
87
+
88
+ ctx.host = host.clone();
89
+
90
+ let accept_encoding = session
91
+ .req_header()
92
+ .headers
93
+ .get("accept-encoding")
94
+ .map(|hv| hv.to_str().unwrap_or(""))
95
+ .unwrap_or("")
96
+ .to_string();
97
+
98
+ let encodings_to_check = self.determine_encodings_to_check(&accept_encoding);
99
+ let query = session
100
+ .req_header()
101
+ .uri
102
+ .query()
103
+ .map_or(String::new(), |q| format!("?{}", q));
104
+ let now_secs = std::time::SystemTime::now()
105
+ .duration_since(std::time::UNIX_EPOCH)
106
+ .unwrap_or(std::time::Duration::from_secs(0))
107
+ .as_secs();
108
+
109
+ self.fetch_from_cache(
110
+ session,
111
+ ctx,
112
+ &host,
113
+ &uri,
114
+ &query,
115
+ &encodings_to_check,
116
+ now_secs,
117
+ )
118
+ .await
119
+ }
120
+
121
+ async fn response_filter(
122
+ &self,
123
+ session: &mut Session,
124
+ resp: &mut ResponseHeader,
125
+ ctx: &mut Self::CTX,
126
+ ) -> Result<()> {
127
+ let req_uri = session.req_header().uri.path();
128
+
129
+ if let Some(rules) = &self.config.cache_control {
130
+ for rule in rules {
131
+ let mut matches = false;
132
+ if let Some(paths) = &rule.paths {
133
+ for p in paths {
134
+ if req_uri.starts_with(p) {
135
+ matches = true;
136
+ break;
137
+ }
138
+ }
139
+ }
140
+ if !matches {
141
+ if let Some(exts) = &rule.extensions {
142
+ for ext in exts {
143
+ if req_uri.ends_with(ext) {
144
+ matches = true;
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ }
150
+ if matches {
151
+ let _ = resp.remove_header("Cache-Control");
152
+ let _ = resp.insert_header("Cache-Control", &rule.value);
153
+ break;
154
+ }
155
+ }
156
+ }
157
+
158
+ let content_type = resp
159
+ .headers
160
+ .get("Content-Type")
161
+ .map(|hv| hv.to_str().unwrap_or(""))
162
+ .unwrap_or("");
163
+
164
+ let default_statuses = vec![200];
165
+ let default_content_types = vec!["text/html".to_string()];
166
+
167
+ let valid_statuses = self
168
+ .config
169
+ .cache
170
+ .as_ref()
171
+ .and_then(|c| c.status.as_ref())
172
+ .unwrap_or(&default_statuses);
173
+
174
+ let valid_content_types = self
175
+ .config
176
+ .cache
177
+ .as_ref()
178
+ .and_then(|c| c.content_types.as_ref())
179
+ .unwrap_or(&default_content_types);
180
+
181
+ let has_valid_status = valid_statuses.contains(&resp.status.as_u16());
182
+ let has_valid_content_type = valid_content_types
183
+ .iter()
184
+ .any(|ct| content_type.contains(ct));
185
+
186
+ if has_valid_status && has_valid_content_type && !ctx.cache_key.is_empty() {
187
+ ctx.should_cache = true;
188
+ ctx.response_header = Some(resp.clone());
189
+ }
190
+
191
+ Ok(())
192
+ }
193
+
194
+ fn response_body_filter(
195
+ &self,
196
+ _session: &mut Session,
197
+ body: &mut Option<Bytes>,
198
+ end_of_stream: bool,
199
+ ctx: &mut Self::CTX,
200
+ ) -> Result<Option<Duration>> {
201
+ if ctx.should_cache {
202
+ if let Some(b) = body {
203
+ ctx.response_body.extend_from_slice(b);
204
+ }
205
+
206
+ if end_of_stream {
207
+ let cache_key = ctx.cache_key.clone();
208
+ let html_bytes = Bytes::from(ctx.response_body.clone());
209
+ let redis_url_opt = self.config.redis_url.clone();
210
+ let t2_ttl = self
211
+ .config
212
+ .cache
213
+ .as_ref()
214
+ .and_then(|c| c.tier2_ttl_seconds)
215
+ .unwrap_or(60);
216
+
217
+ if let Some(header) = ctx.response_header.clone() {
218
+ let host = ctx.host.clone();
219
+ self.spawn_cache_save(
220
+ host,
221
+ cache_key,
222
+ header,
223
+ html_bytes,
224
+ redis_url_opt,
225
+ t2_ttl,
226
+ );
227
+ }
228
+ }
229
+ }
230
+ Ok(None)
231
+ }
232
+ }
@@ -0,0 +1,119 @@
1
+ use async_trait::async_trait;
2
+ use openssl::{pkey::PKey, ssl::NameType, x509::X509};
3
+ use pingora::{
4
+ listeners::{TlsAccept, tls::TlsSettings},
5
+ tls::ext,
6
+ };
7
+ use std::collections::HashMap;
8
+ use tracing::error;
9
+
10
+ pub struct TlsCertificate {
11
+ cert: X509,
12
+ key: PKey<openssl::pkey::Private>,
13
+ chain: Vec<X509>,
14
+ }
15
+
16
+ pub struct DynamicCertificate {
17
+ // Maps SNI hostname to certificate
18
+ certs: HashMap<String, TlsCertificate>,
19
+ default_cert: Option<TlsCertificate>,
20
+ }
21
+
22
+ impl DynamicCertificate {
23
+ pub fn new() -> Self {
24
+ Self {
25
+ certs: HashMap::new(),
26
+ default_cert: None,
27
+ }
28
+ }
29
+ }
30
+
31
+ pub fn new_tls_settings(
32
+ certs_config: Vec<crate::config::CertificateConfig>,
33
+ ) -> Result<TlsSettings, Box<pingora_core::BError>> {
34
+ let mut dynamic_cert = DynamicCertificate::new();
35
+
36
+ for cfg in certs_config {
37
+ let cert_pem = std::fs::read(&cfg.cert_path).map_err(|e| {
38
+ pingora_core::Error::because(
39
+ pingora_core::ErrorType::Custom("TLSConfError"),
40
+ format!("Failed to read cert file {}: {}", cfg.cert_path, e),
41
+ e,
42
+ )
43
+ })?;
44
+ let key_pem = std::fs::read(&cfg.key_path).map_err(|e| {
45
+ pingora_core::Error::because(
46
+ pingora_core::ErrorType::Custom("TLSConfError"),
47
+ format!("Failed to read key file {}: {}", cfg.key_path, e),
48
+ e,
49
+ )
50
+ })?;
51
+
52
+ let cert = X509::from_pem(&cert_pem).map_err(|e| {
53
+ pingora_core::Error::because(
54
+ pingora_core::ErrorType::Custom("TLSConfError"),
55
+ "Failed to parse cert",
56
+ e,
57
+ )
58
+ })?;
59
+ let key = PKey::private_key_from_pem(&key_pem).map_err(|e| {
60
+ pingora_core::Error::because(
61
+ pingora_core::ErrorType::Custom("TLSConfError"),
62
+ "Failed to parse private key",
63
+ e,
64
+ )
65
+ })?;
66
+
67
+ let tls_cert = TlsCertificate {
68
+ cert,
69
+ key,
70
+ chain: vec![], // Extend logic later to handle fullchains if necessary
71
+ };
72
+
73
+ if cfg.host == "default" {
74
+ dynamic_cert.default_cert = Some(tls_cert);
75
+ } else {
76
+ dynamic_cert.certs.insert(cfg.host, tls_cert);
77
+ }
78
+ }
79
+
80
+ let tls =
81
+ TlsSettings::with_callbacks(Box::new(dynamic_cert)).map_err(|e: Box<pingora::Error>| {
82
+ pingora_core::Error::because(
83
+ pingora_core::ErrorType::Custom("TLSConfError"),
84
+ "TlsSettings wrapper error",
85
+ e,
86
+ )
87
+ })?;
88
+ Ok(tls)
89
+ }
90
+
91
+ #[async_trait]
92
+ impl TlsAccept for DynamicCertificate {
93
+ async fn certificate_callback(&self, ssl: &mut pingora::protocols::tls::TlsRef) {
94
+ let server_name = ssl.servername(NameType::HOST_NAME).unwrap_or("localhost");
95
+
96
+ let cert_info = if let Some(cert) = self.certs.get(server_name) {
97
+ cert
98
+ } else if let Some(cert) = &self.default_cert {
99
+ cert
100
+ } else {
101
+ error!("No certificate found for SNI: {}", server_name);
102
+ return;
103
+ };
104
+
105
+ if let Err(e) = ext::ssl_use_certificate(ssl, &cert_info.cert) {
106
+ error!("Failed to use certificate: {}", e);
107
+ }
108
+
109
+ if let Err(e) = ext::ssl_use_private_key(ssl, &cert_info.key) {
110
+ error!("Failed to use private key: {}", e);
111
+ }
112
+
113
+ for chain_cert in &cert_info.chain {
114
+ if let Err(e) = ext::ssl_add_chain_cert(ssl, chain_cert) {
115
+ error!("Failed to add chain certificate: {}", e);
116
+ }
117
+ }
118
+ }
119
+ }