├── sdk ├── rust │ ├── src │ │ ├── lib.rs │ │ └── fbs │ │ │ └── mod.rs │ └── Cargo.toml └── go │ ├── go.mod │ ├── go.sum │ ├── sdk │ ├── logging.go │ ├── response_body_filter.go │ ├── request_filter.go │ ├── response_filter.go │ ├── types.go │ └── constants.go │ └── fbs │ └── nylon_plugin │ ├── RemoveResponseHeader.go │ ├── HeaderKeyValue.go │ └── NylonHttpHeaders.go ├── crates ├── nylon-plugin │ ├── src │ │ ├── native │ │ │ ├── mod.rs │ │ │ └── header_modifier.rs │ │ ├── types.rs │ │ ├── plugin_manager.rs │ │ ├── constants.rs │ │ ├── loaders.rs │ │ └── stream.rs │ └── Cargo.toml ├── nylon-config │ ├── src │ │ ├── lib.rs │ │ ├── utils.rs │ │ ├── services.rs │ │ └── runtime.rs │ └── Cargo.toml ├── nylon-error │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── nylon-command │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── service.rs ├── nylon-tls │ ├── src │ │ ├── lib.rs │ │ ├── certificate.rs │ │ └── metrics.rs │ └── Cargo.toml ├── nylon-types │ ├── src │ │ ├── lib.rs │ │ ├── proxy.rs │ │ ├── tls.rs │ │ ├── route.rs │ │ ├── services.rs │ │ ├── plugins.rs │ │ ├── websocket.rs │ │ └── context.rs │ └── Cargo.toml ├── nylon-store │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── tls.rs │ │ └── websockets.rs └── nylon │ ├── Cargo.toml │ └── src │ ├── backend.rs │ ├── dynamic_certificate.rs │ ├── response.rs │ ├── context.rs │ └── runtime.rs ├── docs ├── .gitignore ├── .vitepress │ ├── theme │ │ ├── index.ts │ │ └── custom.css │ └── config.ts ├── package.json ├── README.md ├── public │ ├── logo.svg │ └── install ├── introduction │ ├── what-is-nylon.md │ ├── quick-start.md │ └── installation.md ├── examples │ └── basic-proxy.md ├── plugins │ └── go-sdk.md └── core │ └── routing.md ├── .gitignore ├── examples ├── go │ ├── go.sum │ ├── go.mod │ └── main.go ├── static │ └── index.html ├── config.yaml ├── proxy │ ├── host_route.yaml │ ├── tls.yaml │ └── base.yaml └── cert │ ├── localhost.crt │ └── localhost.key ├── proto ├── plugin.fbs ├── dispatcher.fbs └── http_context.fbs ├── c └── nylon.h ├── .github ├── FUNDING.yml └── workflows │ └── Ci.yml ├── LICENSE ├── Cargo.toml ├── README.md ├── Makefile └── scripts └── install.sh /sdk/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod fbs; 2 | -------------------------------------------------------------------------------- /crates/nylon-plugin/src/native/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod header_modifier; 2 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vitepress/dist 3 | .vitepress/cache 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | **/*.rs.bk 3 | *.pdb 4 | /target 5 | .config 6 | .DS_Store -------------------------------------------------------------------------------- /crates/nylon-config/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod proxy; 2 | pub mod runtime; 3 | pub mod services; 4 | mod utils; 5 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | import './custom.css' 3 | 4 | export default DefaultTheme 5 | 6 | -------------------------------------------------------------------------------- /sdk/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AssetsArt/nylon/sdk/go 2 | 3 | go 1.24.3 4 | 5 | require github.com/google/flatbuffers v25.2.10+incompatible 6 | -------------------------------------------------------------------------------- /sdk/rust/src/fbs/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | #![allow(clippy::all, clippy::pedantic)] 3 | #![allow(unsafe_op_in_unsafe_fn)] 4 | pub mod plugin_generated; 5 | -------------------------------------------------------------------------------- /crates/nylon-error/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nylon-error" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | thiserror = { workspace = true } 8 | serde_json = { workspace = true } -------------------------------------------------------------------------------- /sdk/go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= 2 | github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 3 | -------------------------------------------------------------------------------- /examples/go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= 2 | github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 3 | -------------------------------------------------------------------------------- /proto/plugin.fbs: -------------------------------------------------------------------------------- 1 | namespace nylon_plugin; 2 | 3 | table HeaderKeyValue { 4 | key: string (required); 5 | value: string (required); 6 | } 7 | 8 | table NylonHttpHeaders { 9 | headers: [HeaderKeyValue] (required); 10 | } 11 | -------------------------------------------------------------------------------- /crates/nylon-command/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nylon-command" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | clap = { workspace = true } 8 | service-manager = { workspace = true } 9 | thiserror = { workspace = true } 10 | tracing = { workspace = true } -------------------------------------------------------------------------------- /crates/nylon-tls/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::type_complexity)] 2 | pub mod acme; 3 | pub mod certificate; 4 | pub mod metrics; 5 | 6 | pub use acme::AcmeClient; 7 | pub use certificate::{CertificateInfo, CertificateStore}; 8 | pub use metrics::{AcmeMetrics, DomainMetrics, MetricsSummary}; 9 | -------------------------------------------------------------------------------- /examples/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AssetsArt/nylon/examples/go 2 | 3 | go 1.24.3 4 | 5 | require github.com/AssetsArt/nylon/sdk/go v0.0.0 6 | 7 | require github.com/google/flatbuffers v25.2.10+incompatible // indirect 8 | 9 | replace github.com/AssetsArt/nylon/sdk/go => ../../sdk/go 10 | -------------------------------------------------------------------------------- /crates/nylon-types/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod context; 2 | pub mod plugins; 3 | pub mod proxy; 4 | pub mod route; 5 | pub mod services; 6 | pub mod template; 7 | pub mod tls; 8 | pub mod websocket; 9 | 10 | /// Nylon runtime server instance 11 | #[derive(Debug, Clone)] 12 | pub struct NylonRuntime {} 13 | -------------------------------------------------------------------------------- /proto/dispatcher.fbs: -------------------------------------------------------------------------------- 1 | namespace nylon_dispatcher; 2 | 3 | table NylonDispatcher { 4 | http_end: bool; 5 | request_id: string (required); 6 | name: string; 7 | entry: string; 8 | data: [ubyte] (required); 9 | payload: [ubyte]; 10 | store: [ubyte]; 11 | } 12 | 13 | root_type NylonDispatcher; 14 | -------------------------------------------------------------------------------- /sdk/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nylon-sdk" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | nylon-types = { path = "../../crates/nylon-types" } 8 | nylon-error = { path = "../../crates/nylon-error" } 9 | flatbuffers = { workspace = true } 10 | pingora = { workspace = true } 11 | bytes = { workspace = true } -------------------------------------------------------------------------------- /examples/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Nylon Static 7 | 8 | 9 |

Nylon Demo

10 |

This page is served by Nylon's static service.

11 | 12 | -------------------------------------------------------------------------------- /sdk/go/sdk/logging.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | func (p *PhaseLogging) Response() *Response { 4 | return &Response{ 5 | ctx: p.ctx, 6 | } 7 | } 8 | 9 | func (p *PhaseLogging) Request() *Request { 10 | return &Request{ 11 | ctx: p.ctx, 12 | } 13 | } 14 | 15 | func (p *PhaseLogging) GetPayload() map[string]any { 16 | return p.ctx.GetPayload() 17 | } 18 | 19 | func (p *PhaseLogging) Next() { 20 | p.ctx.Next() 21 | } 22 | -------------------------------------------------------------------------------- /sdk/go/sdk/response_body_filter.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | func (p *PhaseResponseBodyFilter) Response() *Response { 4 | return &Response{ 5 | ctx: p.ctx, 6 | } 7 | } 8 | 9 | func (p *PhaseResponseBodyFilter) Request() *Request { 10 | return &Request{ 11 | ctx: p.ctx, 12 | } 13 | } 14 | 15 | func (p *PhaseResponseBodyFilter) GetPayload() map[string]any { 16 | return p.ctx.GetPayload() 17 | } 18 | 19 | func (p *PhaseResponseBodyFilter) Next() { 20 | p.ctx.Next() 21 | } 22 | -------------------------------------------------------------------------------- /crates/nylon-config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nylon-config" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | nylon-error = { path = "../nylon-error" } 8 | nylon-store = { path = "../nylon-store" } 9 | nylon-types = { path = "../nylon-types" } 10 | nylon-plugin = { path = "../nylon-plugin" } 11 | serde_yaml_ng = { workspace = true } 12 | serde = { workspace = true } 13 | serde_json = { workspace = true } 14 | num_cpus = { workspace = true } 15 | async-trait = { workspace = true } -------------------------------------------------------------------------------- /c/nylon.h: -------------------------------------------------------------------------------- 1 | #ifndef NYLON_H 2 | #define NYLON_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | typedef struct { 9 | uint32_t sid; 10 | uint8_t phase; 11 | uint32_t method; 12 | const unsigned char *ptr; 13 | uint64_t len; 14 | } FfiBuffer; 15 | 16 | typedef void (*data_event_fn)(const FfiBuffer* ffiBuffer); 17 | 18 | static inline void call_event_method(data_event_fn cb, const FfiBuffer* ffiBuffer) { 19 | cb(ffiBuffer); 20 | } 21 | 22 | #endif // NYLON_H -------------------------------------------------------------------------------- /crates/nylon-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nylon-types" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | nylon-error = { path = "../nylon-error" } 8 | serde = { workspace = true } 9 | serde_json = { workspace = true } 10 | pingora = { workspace = true } 11 | regex = { workspace = true } 12 | chrono = { workspace = true } 13 | uuid = { workspace = true } 14 | bytes = { workspace = true } 15 | libloading = { workspace = true } 16 | async-trait = { workspace = true } 17 | tokio = { workspace = true } 18 | lru = { workspace = true } 19 | once_cell = { workspace = true } -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nylon-docs", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "Documentation for Nylon Reverse Proxy", 6 | "scripts": { 7 | "dev": "vitepress dev", 8 | "build": "npm run copy-install && vitepress build", 9 | "preview": "vitepress preview", 10 | "copy-install": "cp ../scripts/install.sh public/install" 11 | }, 12 | "keywords": ["nylon", "proxy", "reverse-proxy", "documentation"], 13 | "author": "", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "vitepress": "^1.0.0", 17 | "vue": "^3.4.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crates/nylon-tls/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nylon-tls" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | instant-acme = { workspace = true } 8 | tokio = { workspace = true, features = ["full"] } 9 | tracing = { workspace = true } 10 | thiserror = { workspace = true } 11 | openssl = { workspace = true } 12 | serde = { workspace = true } 13 | serde_json = { workspace = true } 14 | chrono = { workspace = true, features = ["serde"] } 15 | rcgen = { workspace = true } 16 | dashmap = { workspace = true } 17 | nylon-types = { path = "../nylon-types" } 18 | nylon-error = { path = "../nylon-error" } 19 | -------------------------------------------------------------------------------- /proto/http_context.fbs: -------------------------------------------------------------------------------- 1 | namespace nylon_http_context; 2 | 3 | table KeyValue { 4 | key: string (required); 5 | value: string (required); 6 | } 7 | 8 | table NylonHttpRequest { 9 | method: string (required); 10 | path: string (required); 11 | query: string; 12 | params: [KeyValue]; 13 | headers: [KeyValue]; 14 | body: [ubyte]; 15 | } 16 | 17 | table NylonHttpResponse { 18 | status: int; 19 | headers: [KeyValue]; 20 | body: [ubyte]; 21 | } 22 | 23 | table NylonHttpContext { 24 | request: NylonHttpRequest (required); 25 | response: NylonHttpResponse (required); 26 | } 27 | 28 | root_type NylonHttpContext; 29 | -------------------------------------------------------------------------------- /crates/nylon-types/src/proxy.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | plugins::PluginItem, 3 | route::{MiddlewareItem, RouteConfig}, 4 | services::ServiceItem, 5 | tls::TlsConfig, 6 | }; 7 | use serde::Deserialize; 8 | use std::collections::HashMap; 9 | 10 | #[derive(Debug, Deserialize, Clone, Default)] 11 | pub struct ProxyConfig { 12 | pub services: Option>, 13 | pub tls: Option>, 14 | pub header_selector: Option, 15 | pub routes: Option>, 16 | pub plugins: Option>, 17 | pub middleware_groups: Option>>, 18 | } 19 | -------------------------------------------------------------------------------- /examples/config.yaml: -------------------------------------------------------------------------------- 1 | http: 2 | - 0.0.0.0:8088 3 | https: 4 | - 0.0.0.0:8443 5 | metrics: 6 | - 127.0.0.1:6192 7 | config_dir: "./examples/proxy" 8 | acme: "./examples/acme" 9 | pingora: 10 | # https://github.com/cloudflare/pingora/blob/main/docs/user_guide/daemon.md 11 | daemon: false 12 | # https://github.com/cloudflare/pingora/blob/main/docs/user_guide/conf.md 13 | grace_period_seconds: 1 14 | graceful_shutdown_timeout_seconds: 1 15 | 16 | # WebSocket adapter configuration (optional) 17 | # websocket: 18 | # adapter_type: redis # memory | redis | cluster 19 | # redis: 20 | # host: "localhost" 21 | # port: 6379 22 | # password: null 23 | # db: 0 24 | # key_prefix: "nylon:ws" -------------------------------------------------------------------------------- /crates/nylon-store/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nylon-store" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | nylon-types = { path = "../nylon-types" } 8 | nylon-error = { path = "../nylon-error" } 9 | nylon-tls = { path = "../nylon-tls" } 10 | dashmap = { workspace = true } 11 | once_cell = { workspace = true } 12 | pingora = { workspace = true } 13 | fnv = { workspace = true } 14 | lru = { workspace = true } 15 | matchit = { workspace = true } 16 | tracing = { workspace = true } 17 | serde_json = { workspace = true } 18 | async-trait = { workspace = true } 19 | tokio = { workspace = true } 20 | tokio-stream = { workspace = true } 21 | uuid = { workspace = true } 22 | chrono = { workspace = true } 23 | redis = { workspace = true } -------------------------------------------------------------------------------- /crates/nylon-plugin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nylon-plugin" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | nylon-types = { path = "../nylon-types" } 8 | nylon-error = { path = "../nylon-error" } 9 | nylon-store = { path = "../nylon-store" } 10 | nylon-sdk = { path = "../../sdk/rust" } 11 | serde_json = { workspace = true } 12 | serde = { workspace = true } 13 | pingora = { workspace = true } 14 | tracing = { workspace = true } 15 | libloading = { workspace = true } 16 | dashmap = { workspace = true } 17 | flatbuffers = { workspace = true } 18 | bytes = { workspace = true } 19 | tokio = { workspace = true } 20 | once_cell = { workspace = true } 21 | async-trait = { workspace = true } 22 | http = { workspace = true } 23 | sha1 = { workspace = true } 24 | base64 = { workspace = true } 25 | chrono = { workspace = true } -------------------------------------------------------------------------------- /crates/nylon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nylon" 3 | version = "1.0.0-beta.4" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | nylon-command = { path = "../nylon-command" } 8 | nylon-config = { path = "../nylon-config" } 9 | nylon-error = { path = "../nylon-error" } 10 | nylon-types = { path = "../nylon-types" } 11 | nylon-store = { path = "../nylon-store" } 12 | nylon-plugin = { path = "../nylon-plugin" } 13 | nylon-tls = { path = "../nylon-tls" } 14 | nylon-sdk = { path = "../../sdk/rust" } 15 | tracing-subscriber = { workspace = true } 16 | tracing = { workspace = true } 17 | pingora = { workspace = true } 18 | async-trait = { workspace = true } 19 | openssl = { workspace = true } 20 | tokio = { workspace = true } 21 | bytes = { workspace = true } 22 | serde_json = { workspace = true } 23 | http = { workspace = true } 24 | flatbuffers = { workspace = true } 25 | dashmap = { workspace = true } 26 | mime_guess = { workspace = true } 27 | fastrand = { workspace = true } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [AssetsArt] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /examples/proxy/host_route.yaml: -------------------------------------------------------------------------------- 1 | 2 | # https://github.com/ibraheemdev/matchit 3 | 4 | # host & path routing 5 | routes: 6 | # App 7 | - route: 8 | type: host 9 | value: localhost # localhost|api.localhost 10 | name: app-route 11 | middleware: 12 | - group: auth 13 | paths: 14 | - path: 15 | - /static 16 | - /static/{*path} 17 | service: 18 | name: static 19 | rewrite: /static 20 | - path: 21 | - /ws 22 | methods: [GET, POST, OPTIONS] 23 | service: 24 | name: ws-service 25 | - path: 26 | - /myapp 27 | - /myapp/{name} 28 | service: 29 | name: myapp-service 30 | - path: 31 | - /stream 32 | methods: [GET, POST, OPTIONS] 33 | service: 34 | name: stream-service 35 | - path: 36 | - / 37 | - /{*path} 38 | middleware: 39 | - group: security 40 | service: 41 | name: app-service -------------------------------------------------------------------------------- /crates/nylon-config/src/utils.rs: -------------------------------------------------------------------------------- 1 | use nylon_error::NylonError; 2 | use std::path::PathBuf; 3 | 4 | pub fn read_dir_recursive(dir: &String, max_depth: u16) -> Result, NylonError> { 5 | let mut files = Vec::new(); 6 | let path_buf = PathBuf::from(dir); 7 | for entry in std::fs::read_dir(path_buf).map_err(|e| { 8 | NylonError::ConfigError(format!("Unable to read config directory {:?}: {}", dir, e)) 9 | })? { 10 | let entry = entry.map_err(|e| { 11 | NylonError::ConfigError(format!( 12 | "Unable to read file in config directory {:?}: {}", 13 | dir, e 14 | )) 15 | })?; 16 | let path = entry.path(); 17 | if path.is_dir() { 18 | if max_depth > 0 { 19 | files.append(&mut read_dir_recursive( 20 | &path.to_string_lossy().to_string(), 21 | max_depth - 1, 22 | )?); 23 | } 24 | } else { 25 | files.push(path); 26 | } 27 | } 28 | Ok(files) 29 | } 30 | -------------------------------------------------------------------------------- /sdk/go/sdk/request_filter.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | func (ctx *PhaseRequestFilter) Request() *Request { 4 | return &Request{ 5 | ctx: ctx.ctx, 6 | } 7 | } 8 | 9 | func (ctx *PhaseRequestFilter) Response() *Response { 10 | return &Response{ 11 | ctx: ctx.ctx, 12 | } 13 | } 14 | 15 | func (p *PhaseRequestFilter) GetPayload() map[string]any { 16 | return p.ctx.GetPayload() 17 | } 18 | 19 | func (p *PhaseRequestFilter) Next() { 20 | p.ctx.Next() 21 | } 22 | 23 | func (p *PhaseRequestFilter) End() { 24 | p.ctx.End() 25 | } 26 | 27 | // WebSocket helpers 28 | func (p *PhaseRequestFilter) WebSocketUpgrade(cbs WebSocketCallbacks) error { 29 | // Store callbacks in context for dispatch before requesting upgrade 30 | // This ensures callbacks are available when events arrive 31 | p.ctx.mu.Lock() 32 | p.ctx.wsCallbacks = &cbs 33 | p.ctx.wsUpgraded = false // Reset state before upgrade 34 | p.ctx.mu.Unlock() 35 | 36 | // Ask Rust to upgrade - this will trigger OnOpen event after handshake 37 | return RequestMethod(p.ctx.sessionID, 0, NylonMethodWebSocketUpgrade, nil) 38 | } 39 | -------------------------------------------------------------------------------- /crates/nylon-command/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod handler; 2 | mod service; 3 | 4 | use clap::{Parser, Subcommand}; 5 | 6 | pub use handler::{ServiceError, handle_service_command}; 7 | pub use service::ServiceCommands; 8 | 9 | #[derive(Parser, Debug)] 10 | #[command(version, about, long_about = None)] 11 | pub struct Cli { 12 | #[command(subcommand)] 13 | pub command: Commands, 14 | } 15 | 16 | #[derive(Debug, Subcommand)] 17 | pub enum Commands { 18 | #[command(name = "service", short_flag = 's')] 19 | #[command(about = "Manage the proxy daemon service (install, start, stop, etc.)")] 20 | #[command(subcommand)] 21 | Service(ServiceCommands), 22 | 23 | // run with no command 24 | #[command(name = "run")] 25 | #[command(about = "Run the proxy server with a config file")] 26 | Run { 27 | #[arg(long, short = 'c', default_value = "/etc/nylon/config.yaml")] 28 | #[arg(help = "Path to the config file example: /etc/nylon/config.yaml")] 29 | config: String, 30 | }, 31 | } 32 | 33 | pub fn parse() -> Cli { 34 | Cli::parse() 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 AssetsArt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /crates/nylon/src/backend.rs: -------------------------------------------------------------------------------- 1 | use nylon_error::NylonError; 2 | use nylon_store::lb_backends::{BackendType, HttpService}; 3 | use nylon_types::context::NylonContext; 4 | use pingora::{lb::Backend, proxy::Session}; 5 | 6 | pub fn selection( 7 | service: &HttpService, 8 | session: &mut Session, 9 | ctx: &mut NylonContext, 10 | ) -> Result { 11 | let mut selection_key = ctx.client_ip.read().expect("lock").clone(); 12 | if let Some(header_value) = session.req_header().headers.get("x-forwarded-for") { 13 | let value = header_value.to_str().unwrap_or_default(); 14 | selection_key.push_str(value); 15 | } 16 | match &service.backend_type { 17 | BackendType::RoundRobin(lb) => lb.select(selection_key.as_bytes(), 256), 18 | BackendType::Weighted(lb) => lb.select(selection_key.as_bytes(), 256), 19 | BackendType::Consistent(lb) => lb.select(selection_key.as_bytes(), 256), 20 | BackendType::Random(lb) => lb.select(selection_key.as_bytes(), 256), 21 | } 22 | .ok_or(NylonError::HttpException( 23 | 500, 24 | "INTERNAL_SERVER_ERROR", 25 | "No backend found", 26 | )) 27 | } 28 | -------------------------------------------------------------------------------- /examples/cert/localhost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDDzCCAfegAwIBAgIUNr7z1bLoetY9ZXYtiF/hfZQ9K9owDQYJKoZIhvcNAQEL 3 | BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDYxNDA1NTQyOFoXDTI1MDcx 4 | NDA1NTQyOFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF 5 | AAOCAQ8AMIIBCgKCAQEA2QZEci0Vp/symjYz9wGIvDz1CXpsr125c/lVCo4jEpaf 6 | pQajXMTDGOnWQr4R/RHzhFarZsb6kL/ot0luqSj1L/tp7The8u5rZZNTu+N+xeWK 7 | RVsIEtplkqzFSE83fLXezKtBGkU3rG/GHC3+KJVz4hY5IqvcP7yLV/S7SX97twNk 8 | strMueeG10x3FHxtA5weF1eqslD6Iu6t4VMgfMvK4Jx1Jphyx37FlP5ER7cy7uh1 9 | lD3bNAwotm4jVKXYCAMeCjpOHHqVoOOSOwdGZFM+fEx6TakiXCBCUtRZxXCG4N5T 10 | JDdoYj+L3PeI5YBKfv0npu2j/icngEqnN6K6Muk1YQIDAQABo1kwVzAUBgNVHREE 11 | DTALgglsb2NhbGhvc3QwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMB 12 | MB0GA1UdDgQWBBRCm2GHqEaqrZnndTpzJnHC9NMwsTANBgkqhkiG9w0BAQsFAAOC 13 | AQEAXf/YDLqzm21l8BzjGBHXVzR0cf3qajxUcpD3y20tCNBtr+ijLnCPjKGrAfr5 14 | 9ApB74ZijCk90wvwNsxtgNdFadBmdM9epZRrNkqzEn65+B4SVuD35kgos/I9dS1E 15 | 14qAeBUZYwcSz4XekZfR6/cPHrsuvQGrRu5E2bv1p1oSaE1DAzuXav0dDabMoDC8 16 | p39MXumqetIHHpqyWo4UUsiOvyFQdModviNW77up1PYjWxHPmUqKnHf/13N74HHa 17 | CRmmRQcErbWcRPxI8BBPrX0UgRHzyeZaPDfVa7he8SV0696Is/S69yhH/sJ80A0n 18 | GTIaeFOA9dp4k1FLGKRnyxGLTA== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /crates/nylon-command/src/service.rs: -------------------------------------------------------------------------------- 1 | use clap::Subcommand; 2 | 3 | #[derive(Debug, Subcommand)] 4 | pub enum ServiceCommands { 5 | // Install the service 6 | #[command(name = "install")] 7 | #[command(about = "Install the service.")] 8 | Install, 9 | 10 | // Uninstall the service 11 | #[command(name = "uninstall")] 12 | #[command(about = "Uninstall the service.")] 13 | Uninstall, 14 | 15 | // Start the service 16 | #[command(name = "start")] 17 | #[command(about = "Start the service and apply all configuration changes.")] 18 | Start, 19 | 20 | // Stop the service 21 | #[command(name = "stop")] 22 | #[command(about = "Stop the service.")] 23 | Stop, 24 | 25 | // Restart the service 26 | #[command(name = "restart")] 27 | #[command(about = "Restart the service and apply all configuration changes.")] 28 | Restart, 29 | 30 | // Status of the service 31 | #[command(name = "status")] 32 | #[command(about = "Show the status of the service.")] 33 | Status, 34 | 35 | // Reload the service 36 | #[command(name = "reload")] 37 | #[command( 38 | about = "Reload route and service configurations without applying global configuration changes." 39 | )] 40 | Reload, 41 | } 42 | -------------------------------------------------------------------------------- /sdk/go/sdk/response_filter.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | func (p *PhaseResponseFilter) Response() *Response { 4 | return &Response{ 5 | ctx: p.ctx, 6 | } 7 | } 8 | 9 | func (p *PhaseResponseFilter) Request() *Request { 10 | return &Request{ 11 | ctx: p.ctx, 12 | } 13 | } 14 | 15 | func (p *PhaseResponseFilter) SetResponseHeader(key, value string) { 16 | httpCtx := Response{ 17 | ctx: p.ctx, 18 | } 19 | httpCtx.SetHeader(key, value) 20 | } 21 | 22 | func (p *PhaseResponseFilter) RemoveResponseHeader(key string) { 23 | httpCtx := Response{ 24 | ctx: p.ctx, 25 | } 26 | httpCtx.RemoveHeader(key) 27 | } 28 | 29 | func (p *PhaseResponseFilter) SetResponseStatus(status uint16) { 30 | httpCtx := Response{ 31 | ctx: p.ctx, 32 | } 33 | httpCtx.SetStatus(status) 34 | } 35 | 36 | func (p *PhaseResponseFilter) GetRequestHeader(key string) string { 37 | httpCtx := Request{ 38 | ctx: p.ctx, 39 | } 40 | return httpCtx.Headers().Get(key) 41 | } 42 | 43 | func (p *PhaseResponseFilter) GetRequestHeaders() map[string]string { 44 | httpCtx := Request{ 45 | ctx: p.ctx, 46 | } 47 | return httpCtx.Headers().GetAll() 48 | } 49 | 50 | func (p *PhaseResponseFilter) GetPayload() map[string]any { 51 | return p.ctx.GetPayload() 52 | } 53 | 54 | func (p *PhaseResponseFilter) Next() { 55 | p.ctx.Next() 56 | } 57 | -------------------------------------------------------------------------------- /crates/nylon-plugin/src/types.rs: -------------------------------------------------------------------------------- 1 | //! Types and structures used throughout the plugin system 2 | 3 | use nylon_types::{route::MiddlewareItem, template::Expr}; 4 | use serde_json::Value; 5 | use std::collections::HashMap; 6 | 7 | /// Built-in plugins that are available by default 8 | #[derive(Debug, Clone, PartialEq)] 9 | pub enum BuiltinPlugin { 10 | RequestHeaderModifier, 11 | ResponseHeaderModifier, 12 | } 13 | 14 | /// Context for middleware execution 15 | #[derive(Debug, Clone)] 16 | pub struct MiddlewareContext { 17 | pub middleware: MiddlewareItem, 18 | pub payload: Option, 19 | pub payload_ast: Option>>, 20 | } 21 | 22 | /// Result of plugin execution 23 | #[derive(Debug, Clone, Default)] 24 | pub struct PluginResult { 25 | pub http_end: bool, 26 | pub stream_end: bool, 27 | } 28 | 29 | impl PluginResult { 30 | /// Create a new plugin result 31 | pub fn new(http_end: bool, stream_end: bool) -> Self { 32 | Self { 33 | http_end, 34 | stream_end, 35 | } 36 | } 37 | } 38 | 39 | /// Plugin execution context 40 | #[derive(Debug)] 41 | pub struct PluginExecutionContext<'a> { 42 | pub plugin_name: &'a str, 43 | pub entry: &'a str, 44 | pub payload: &'a Option>, 45 | pub params: &'a Option>, 46 | } 47 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "3" 3 | 4 | members = [ 5 | "crates/*", "sdk/rust", 6 | ] 7 | 8 | [workspace.dependencies] 9 | thiserror = "2.0" 10 | clap = { version = "4.5", features = ["derive"] } 11 | tracing-subscriber = "0.3" 12 | tracing = "0.1" 13 | serde_yaml_ng = "0.10" 14 | serde = { version = "1.0", features = ["derive"] } 15 | serde_json = "1.0" 16 | num_cpus = "1.0" 17 | dashmap = "6.1.0" 18 | once_cell = "1.20" 19 | pingora = { version="0.6", features = ["lb", "openssl"] } 20 | async-trait = "0.1" 21 | lru = "0.16" 22 | openssl = { version = "0.10", features = ["vendored"] } 23 | tokio = { version = "1", features = ["rt"] } 24 | tokio-stream = "0.1" 25 | fnv = "1.0" 26 | matchit = "0.8" 27 | bytes = "1.7" 28 | http = "1.3" 29 | regex = "1.11" 30 | chrono = "0.4" 31 | uuid = { version = "1.13", features = ["v4", "v7"] } 32 | flatbuffers = { version = "25.9", features = ["serde"] } 33 | libloading = "0.8.9" 34 | instant-acme = "0.8" 35 | sha1 = "0.10" 36 | base64 = { version = "0.22", default-features = false, features = ["alloc"] } 37 | redis = { version = "0.32", features = ["aio", "tokio-comp"] } 38 | mime_guess = "2.0" 39 | rcgen = "0.14" 40 | fastrand = "2.1" 41 | service-manager = "0.8" 42 | 43 | [profile.release] 44 | overflow-checks = true 45 | strip = true 46 | opt-level = "z" 47 | lto = true 48 | codegen-units = 1 49 | panic = "abort" 50 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Nylon Documentation 2 | 3 | This directory contains the documentation for Nylon Reverse Proxy, built with [VitePress](https://vitepress.dev/). 4 | 5 | ## Development 6 | 7 | ```bash 8 | # Install dependencies 9 | bun install 10 | 11 | # Start dev server 12 | bun run dev 13 | 14 | # Build for production 15 | bun run build 16 | 17 | # Preview production build 18 | bun run preview 19 | ``` 20 | 21 | ## Structure 22 | 23 | ``` 24 | docs/ 25 | ├── .vitepress/ # VitePress configuration 26 | │ ├── config.ts # Site configuration 27 | │ └── theme/ # Custom theme 28 | ├── introduction/ # Getting started guides 29 | ├── core/ # Core concepts 30 | ├── plugins/ # Plugin development 31 | ├── examples/ # Usage examples 32 | └── api/ # API reference 33 | ``` 34 | 35 | ## Writing Documentation 36 | 37 | - Use Markdown (.md) files 38 | - Follow the existing structure 39 | - Add new pages to `.vitepress/config.ts` sidebar 40 | - Use code blocks with language hints 41 | - Add examples where appropriate 42 | 43 | ## Deployment 44 | 45 | The documentation can be deployed to any static hosting service: 46 | 47 | - GitHub Pages 48 | - Netlify 49 | - Vercel 50 | - Cloudflare Pages 51 | 52 | Build the site with `npm run build` and deploy the `.vitepress/dist` directory. 53 | 54 | -------------------------------------------------------------------------------- /sdk/go/sdk/types.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type HttpPluginFunc func(ctx *NylonHttpPluginCtx) 8 | 9 | type NylonPlugin struct{} 10 | 11 | type NylonHttpPluginCtx struct { 12 | sessionID int32 13 | 14 | mu sync.Mutex 15 | cond *sync.Cond 16 | dataMap map[uint32][]byte 17 | 18 | // WebSocket state 19 | wsCallbacks *WebSocketCallbacks 20 | wsUpgraded bool 21 | } 22 | 23 | type Headers struct { 24 | headers map[string]string 25 | } 26 | 27 | type Response struct { 28 | ctx *NylonHttpPluginCtx 29 | } 30 | 31 | type Request struct { 32 | ctx *NylonHttpPluginCtx 33 | } 34 | 35 | type ResponseStream struct { 36 | response *Response 37 | } 38 | 39 | type PhaseRequestFilter struct { 40 | ctx *NylonHttpPluginCtx 41 | } 42 | 43 | type PhaseResponseFilter struct { 44 | ctx *NylonHttpPluginCtx 45 | } 46 | 47 | type PhaseResponseBodyFilter struct { 48 | ctx *NylonHttpPluginCtx 49 | } 50 | 51 | type PhaseLogging struct { 52 | ctx *NylonHttpPluginCtx 53 | } 54 | 55 | // WebSocket types 56 | 57 | type WebSocketConn struct { 58 | ctx *NylonHttpPluginCtx 59 | } 60 | 61 | type WebSocketCallbacks struct { 62 | OnOpen func(ws *WebSocketConn) 63 | OnMessageText func(ws *WebSocketConn, msg string) 64 | OnMessageBinary func(ws *WebSocketConn, data []byte) 65 | OnClose func(ws *WebSocketConn) 66 | OnError func(ws *WebSocketConn, err string) 67 | } 68 | -------------------------------------------------------------------------------- /crates/nylon-types/src/tls.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize, Clone, PartialEq)] 4 | pub enum TlsKind { 5 | #[serde(rename = "custom")] 6 | Custom, 7 | #[serde(rename = "acme")] 8 | Acme, 9 | } 10 | 11 | #[derive(Debug, Deserialize, Clone)] 12 | pub struct TlsConfig { 13 | #[serde(rename = "type")] 14 | pub kind: TlsKind, // "custom" or "acme" 15 | pub key: Option, 16 | pub cert: Option, 17 | pub chain: Option>, 18 | pub acme: Option, 19 | /// Flatten ACME fields to support both nested and flat config formats 20 | #[serde(flatten)] 21 | pub acme_flat: Option, 22 | pub domains: Vec, 23 | } 24 | 25 | #[derive(Debug, Deserialize, Clone)] 26 | pub struct AcmeConfig { 27 | pub provider: String, 28 | pub email: String, 29 | /// Path to ACME directory (will use runtime config if not specified) 30 | #[serde(skip)] 31 | pub acme_dir: Option, 32 | /// Use staging endpoint if provider supports it (e.g. Let's Encrypt) 33 | pub staging: Option, 34 | /// Custom ACME directory URL (overrides provider/staging) 35 | pub directory_url: Option, 36 | /// External Account Binding key identifier (for providers like ZeroSSL) 37 | pub eab_kid: Option, 38 | /// External Account Binding HMAC key (base64/urlsafe as required by provider) 39 | pub eab_hmac_key: Option, 40 | } 41 | -------------------------------------------------------------------------------- /crates/nylon-types/src/route.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde_json::Value; 3 | 4 | pub const HTTP_METHODS: [&str; 9] = [ 5 | "GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "CONNECT", "TRACE", "PATCH", 6 | ]; 7 | 8 | #[derive(Debug, Deserialize, Clone)] 9 | pub struct MiddlewareItem { 10 | pub group: Option, 11 | pub plugin: Option, 12 | pub entry: Option, 13 | pub payload: Option, 14 | } 15 | 16 | #[derive(Debug, Deserialize, Clone)] 17 | pub struct RouteConfig { 18 | pub route: RouteMatcher, 19 | pub name: String, 20 | pub tls: Option, 21 | pub middleware: Option>, 22 | pub paths: Vec, 23 | } 24 | 25 | #[derive(Debug, Deserialize, Clone)] 26 | pub struct RouteMatcher { 27 | #[serde(rename = "type")] 28 | pub kind: String, 29 | pub value: String, 30 | } 31 | 32 | #[derive(Debug, Deserialize, Clone)] 33 | pub struct TlsRoute { 34 | pub enabled: bool, 35 | pub redirect: Option, 36 | } 37 | 38 | #[derive(Debug, Deserialize, Clone)] 39 | pub struct Header { 40 | pub name: String, 41 | pub value: String, 42 | } 43 | 44 | #[derive(Debug, Deserialize, Clone)] 45 | pub struct PathConfig { 46 | pub path: Value, 47 | pub service: ServiceRef, 48 | pub middleware: Option>, 49 | pub methods: Option>, 50 | } 51 | 52 | #[derive(Debug, Deserialize, Clone)] 53 | pub struct ServiceRef { 54 | pub name: String, 55 | pub rewrite: Option, 56 | } 57 | -------------------------------------------------------------------------------- /crates/nylon-store/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod lb_backends; 2 | pub mod redis_adapter; 3 | pub mod routes; 4 | pub mod tls; 5 | pub mod websocket_adapter; 6 | pub mod websockets; 7 | 8 | use dashmap::DashMap; 9 | use once_cell::sync::Lazy; 10 | use std::any::Any; 11 | 12 | // default values 13 | pub const DEFAULT_HEADER_SELECTOR: &str = "x-nylon-proxy"; 14 | 15 | // constants 16 | pub const KEY_RUNTIME_CONFIG: &str = "runtime_config"; 17 | pub const KEY_CONFIG_PATH: &str = "config_path"; 18 | pub const KEY_COMMAND_SOCKET_PATH: &str = "/tmp/_nylon.sock"; 19 | pub const KEY_LB_BACKENDS: &str = "lb_backends"; 20 | pub const KEY_ROUTES: &str = "routes"; 21 | pub const KEY_TLS_ROUTES: &str = "tls_routes"; 22 | pub const KEY_ROUTES_MATCHIT: &str = "routes_matchit"; 23 | pub const KEY_HEADER_SELECTOR: &str = "header_selector"; 24 | pub const KEY_LIBRARY_FILE: &str = "library_file"; 25 | pub const KEY_PLUGINS: &str = "plugins"; 26 | pub const KEY_TLS: &str = "tls"; 27 | pub const KEY_ACME_CERTS: &str = "acme_certs"; 28 | pub const KEY_ACME_CONFIG: &str = "acme_config"; 29 | pub const KEY_ACME_METRICS: &str = "acme_metrics"; 30 | 31 | // storage for global variables 32 | static GLOBAL_STORE: Lazy>> = Lazy::new(DashMap::new); 33 | 34 | pub fn insert(key: &str, value: T) { 35 | GLOBAL_STORE.insert(key.to_string(), Box::new(value)); 36 | } 37 | 38 | pub fn get(key: &str) -> Option { 39 | let entry = GLOBAL_STORE.get(key)?; 40 | let any_ref = entry.downcast_ref::()?; 41 | Some(any_ref.clone()) 42 | } 43 | -------------------------------------------------------------------------------- /examples/proxy/tls.yaml: -------------------------------------------------------------------------------- 1 | tls: 2 | # Custom TLS - Use your own certificate files 3 | - type: custom 4 | cert: ./examples/cert/localhost.crt 5 | key: ./examples/cert/localhost.key 6 | # chain: # Optional - Certificate chain for intermediate CAs 7 | # - ./examples/cert/chain.pem 8 | domains: 9 | - localhost 10 | - app.localhost 11 | - api.localhost 12 | - admin.localhost 13 | 14 | # ACME (Let's Encrypt) - Automatic certificate management 15 | # Requires: 16 | # - Port 80 accessible from internet for HTTP-01 challenge 17 | # - Valid DNS records pointing to your server 18 | # - Email for certificate expiration notifications 19 | # 20 | # Features: 21 | # - Automatic certificate issuance and renewal (30 days before expiry) 22 | # - Rate limiting protection with exponential backoff 23 | # - Challenge token auto-cleanup 24 | # - Comprehensive metrics tracking 25 | # - Retry logic with jitter for failed renewals 26 | # 27 | # - type: acme 28 | # provider: letsencrypt # Options: letsencrypt 29 | # email: admin@example.com # Required for account registration 30 | # # staging: true # Use Let's Encrypt staging for testing (optional) 31 | # # directory_url: https://acme.example.com/directory # Custom ACME server (optional) 32 | # domains: 33 | # - example.com 34 | # - api.example.com 35 | # - admin.example.com 36 | # 37 | # Advanced options (for providers like ZeroSSL, BuyPass): 38 | # Note: Full EAB support is planned but not yet fully implemented 39 | # # eab_kid: your-eab-key-id # External Account Binding key ID 40 | # # eab_hmac_key: your-eab-hmac-key # EAB HMAC key -------------------------------------------------------------------------------- /examples/cert/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDZBkRyLRWn+zKa 3 | NjP3AYi8PPUJemyvXblz+VUKjiMSlp+lBqNcxMMY6dZCvhH9EfOEVqtmxvqQv+i3 4 | SW6pKPUv+2ntOF7y7mtlk1O7437F5YpFWwgS2mWSrMVITzd8td7Mq0EaRTesb8Yc 5 | Lf4olXPiFjkiq9w/vItX9LtJf3u3A2Sy2sy554bXTHcUfG0DnB4XV6qyUPoi7q3h 6 | UyB8y8rgnHUmmHLHfsWU/kRHtzLu6HWUPds0DCi2biNUpdgIAx4KOk4cepWg45I7 7 | B0ZkUz58THpNqSJcIEJS1FnFcIbg3lMkN2hiP4vc94jlgEp+/Sem7aP+JyeASqc3 8 | oroy6TVhAgMBAAECggEAGPIfmrysLzaAeOmnVDqCyjlbBKuD8+3q5+XmZKvVI/k4 9 | kUZnr32F+/Z//IS+ylc+ha0VL1+KlGocwLmx/MOCmHD2mnAG+PdXFLJINxIFo894 10 | UvPNZCZis8cEd8T0SLNakG3UckW3yixAakOOogFY1Elv6Jh23QQqA7KTtxVuZekp 11 | miAS6Gdotoz7aiCVNAnw6YKABvheDfRglniCInqZto9MtuPGk/LFcPDVvHVUQVMG 12 | KR5klwj4NZ6HglIk3UT4N4kk8NXBa3ndzZDJq7Lfm3Ns7bHPghrYzD/ABut6R3To 13 | 0MEkCwTlZj5K8d/IT8K77x9B7fqI1GSXusSAlGziWQKBgQDzdP+6KPf3KLORqIqF 14 | k1AakW5+ofwHYgcfes5ZJEpBQAvonJ+dkJkC7HdKoWtpPpbXrMilXdu5jk+hvwC5 15 | Sle9q/M7Pnv9jXLjz+1WjqOScTKy4uvWM+8PX1mOq6/o31gd/3nVwdszLdy3g9DC 16 | VK6X8619bN7PD2QCZqM12QYKmQKBgQDkNKUCIKT2zSD5iTi6BSoCzXhC9O6i+bW2 17 | 0fhsoL38vW/tseGj8aHNSbEfA0AwJJvEzTVOZFsmvVpKhRGXsLBx1MtuRAszoFif 18 | 294LzaooB1gtIhnDY9vjLSoiHPfJV1ASUEmLOtBbPrhLSVq8xithznTGtJwBiAZi 19 | Xe51VlVGCQKBgQC6NkAvVIytOC14+K/TEWUQnTIlm6JYx0rpchYIqrA9Dk7NgZa4 20 | ftP6H4HyzFqKqjvYBSmHCq44VDhmX+Ce2NUZlz64jsdpnVpGE1DWhs1oAjskBlsa 21 | gKiWWnj2ni0zcjlE4JaAwAD4OVj76M+xA/Jy+Qg2yiH1wDDfgT/OvQtY6QKBgQCY 22 | yJo063AmgD10c6eT+0MeLzw179AZMv+yz67340JvhND8HZzI60x9qbm43q9JzCix 23 | wQXQXyYbsKhTvfWCTlxDScmNIGczgEX1ePmXg3FJbWlehjcjdqbP2PwdbLGEjj1g 24 | lXo3if/XJw2x8gGa4z5GNDhAlMjhyZUkpGizDEL5KQKBgQDM6q1/ZJIiPQ9UeUH9 25 | /WgyNI1MgnDdwTC7GjcLSX45DOzv5FCIB0DLC7qcjFY2Xp4hlE9YZs9asE3x4TG1 26 | EbvTTAu/BnLxCDPKQp6FvjqyfrSn/wIcUvB+ZyzGw2PMwzG8DCrIYWopvNSKsHEJ 27 | RKnKARSYSjyCr+Xh7UexNHmdLA== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /sdk/go/fbs/nylon_plugin/RemoveResponseHeader.go: -------------------------------------------------------------------------------- 1 | // Code generated by the FlatBuffers compiler. DO NOT EDIT. 2 | 3 | package nylon_plugin 4 | 5 | import ( 6 | flatbuffers "github.com/google/flatbuffers/go" 7 | ) 8 | 9 | type RemoveResponseHeader struct { 10 | _tab flatbuffers.Table 11 | } 12 | 13 | func GetRootAsRemoveResponseHeader(buf []byte, offset flatbuffers.UOffsetT) *RemoveResponseHeader { 14 | n := flatbuffers.GetUOffsetT(buf[offset:]) 15 | x := &RemoveResponseHeader{} 16 | x.Init(buf, n+offset) 17 | return x 18 | } 19 | 20 | func FinishRemoveResponseHeaderBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { 21 | builder.Finish(offset) 22 | } 23 | 24 | func GetSizePrefixedRootAsRemoveResponseHeader(buf []byte, offset flatbuffers.UOffsetT) *RemoveResponseHeader { 25 | n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) 26 | x := &RemoveResponseHeader{} 27 | x.Init(buf, n+offset+flatbuffers.SizeUint32) 28 | return x 29 | } 30 | 31 | func FinishSizePrefixedRemoveResponseHeaderBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { 32 | builder.FinishSizePrefixed(offset) 33 | } 34 | 35 | func (rcv *RemoveResponseHeader) Init(buf []byte, i flatbuffers.UOffsetT) { 36 | rcv._tab.Bytes = buf 37 | rcv._tab.Pos = i 38 | } 39 | 40 | func (rcv *RemoveResponseHeader) Table() flatbuffers.Table { 41 | return rcv._tab 42 | } 43 | 44 | func (rcv *RemoveResponseHeader) Key() []byte { 45 | o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) 46 | if o != 0 { 47 | return rcv._tab.ByteVector(o + rcv._tab.Pos) 48 | } 49 | return nil 50 | } 51 | 52 | func RemoveResponseHeaderStart(builder *flatbuffers.Builder) { 53 | builder.StartObject(1) 54 | } 55 | func RemoveResponseHeaderAddKey(builder *flatbuffers.Builder, key flatbuffers.UOffsetT) { 56 | builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(key), 0) 57 | } 58 | func RemoveResponseHeaderEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { 59 | return builder.EndObject() 60 | } 61 | -------------------------------------------------------------------------------- /crates/nylon-types/src/services.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize, Clone)] 4 | pub struct HealthCheck { 5 | pub enabled: bool, 6 | pub path: String, 7 | pub interval: String, 8 | pub timeout: String, 9 | pub healthy_threshold: u32, 10 | pub unhealthy_threshold: u32, 11 | } 12 | 13 | #[derive(Debug, Deserialize, Clone)] 14 | pub struct Endpoint { 15 | pub ip: String, 16 | pub port: u16, 17 | pub weight: Option, 18 | } 19 | 20 | #[derive(Debug, Deserialize, Clone, PartialEq)] 21 | pub enum ServiceType { 22 | #[serde(rename = "http")] 23 | Http, 24 | #[serde(rename = "plugin")] 25 | Plugin, 26 | #[serde(rename = "static")] 27 | Static, 28 | } 29 | 30 | #[derive(Debug, Deserialize, Clone)] 31 | pub enum Algorithm { 32 | #[serde(rename = "round_robin")] 33 | RoundRobin, 34 | #[serde(rename = "random")] 35 | Random, 36 | #[serde(rename = "consistent")] 37 | Consistent, 38 | #[serde(rename = "weighted")] 39 | Weighted, 40 | } 41 | 42 | #[derive(Debug, Deserialize, Clone)] 43 | pub struct Plugin { 44 | pub name: String, 45 | pub entry: String, 46 | pub payload: Option, 47 | } 48 | 49 | #[derive(Debug, Deserialize, Clone)] 50 | pub struct StaticConfig { 51 | /// Root directory to serve files from 52 | pub root: String, 53 | /// Default index file to serve for directories (default: index.html) 54 | pub index: Option, 55 | /// Enable SPA fallback: on 404, serve index file instead 56 | pub spa: Option, 57 | } 58 | 59 | #[derive(Debug, Deserialize, Clone)] 60 | pub struct ServiceItem { 61 | pub name: String, 62 | pub service_type: ServiceType, 63 | pub algorithm: Option, 64 | pub endpoints: Option>, 65 | pub health_check: Option, 66 | pub plugin: Option, 67 | #[serde(rename = "static")] 68 | pub static_conf: Option, 69 | } 70 | -------------------------------------------------------------------------------- /crates/nylon-config/src/services.rs: -------------------------------------------------------------------------------- 1 | use nylon_error::NylonError; 2 | use nylon_types::services::{Endpoint, HealthCheck}; 3 | use std::net::IpAddr; 4 | 5 | pub trait EndpointExt { 6 | fn is_valid_ip(&self) -> Result<(), NylonError>; 7 | } 8 | 9 | pub trait HealthCheckExt { 10 | fn is_valid(&self) -> Result<(), NylonError>; 11 | } 12 | 13 | impl EndpointExt for Endpoint { 14 | fn is_valid_ip(&self) -> Result<(), NylonError> { 15 | match self.ip.parse::() { 16 | Ok(_) => Ok(()), 17 | Err(err) => Err(NylonError::ConfigError(format!( 18 | "Invalid IP address: {}", 19 | err 20 | ))), 21 | } 22 | } 23 | } 24 | 25 | impl HealthCheckExt for HealthCheck { 26 | fn is_valid(&self) -> Result<(), NylonError> { 27 | if self.interval.is_empty() { 28 | return Err(NylonError::ConfigError("Interval must be set".to_string())); 29 | } 30 | if self.timeout.is_empty() { 31 | return Err(NylonError::ConfigError("Timeout must be set".to_string())); 32 | } 33 | if self.healthy_threshold == 0 { 34 | return Err(NylonError::ConfigError( 35 | "Healthy threshold must be set".to_string(), 36 | )); 37 | } 38 | if self.unhealthy_threshold == 0 { 39 | return Err(NylonError::ConfigError( 40 | "Unhealthy threshold must be set".to_string(), 41 | )); 42 | } 43 | if self.path.is_empty() { 44 | return Err(NylonError::ConfigError("Path must be set".to_string())); 45 | } 46 | if !self.interval.ends_with("s") { 47 | return Err(NylonError::ConfigError( 48 | "Interval must be in the format of [0-9]+[s]".to_string(), 49 | )); 50 | } 51 | if !self.timeout.ends_with("s") { 52 | return Err(NylonError::ConfigError( 53 | "Timeout must be in the format of [0-9]+[s]".to_string(), 54 | )); 55 | } 56 | Ok(()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🧬 Nylon — The Extensible Proxy Server 2 | 3 | [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) 4 | [![Docs](https://img.shields.io/badge/docs-online-blue)](https://nylon.sh/) 5 | 6 | Nylon is a lightweight, high‑performance, and extensible proxy built on top of the battle‑tested [Cloudflare Pingora](https://github.com/cloudflare/pingora) framework. 7 | 8 | ## ✨ Features 9 | 10 | - **🔌 Extensible**: Write plugins in Go, Rust, Zig, C via FFI 11 | - **📝 Simple YAML Config**: One place to manage routes, services, middleware 12 | - **🎯 Smart Routing**: Host/header/path matching with multiple load balancing algorithms 13 | - **🔒 TLS Built-in**: Custom certificates or ACME (Let's Encrypt, Buypass) 14 | - **☁️ Cloud-native**: Observability and scalability friendly 15 | - **⚡ High Performance**: Built on Cloudflare Pingora framework 16 | 17 | ## 🚀 Quick Start 18 | 19 | ```sh 20 | # Install (macOS/Linux) 21 | curl -fsSL https://nylon.sh/install | sh 22 | 23 | # Run with example config 24 | nylon run -c ./examples/config.yaml 25 | ``` 26 | 27 | Test it: 28 | ```sh 29 | curl http://127.0.0.1:8088/ 30 | ``` 31 | 32 | ## 📖 Documentation 33 | 34 | For complete documentation, visit **[nylon.sh](https://nylon.sh/)** 35 | 36 | - [Installation Guide](https://nylon.sh/introduction/installation) 37 | - [Quick Start](https://nylon.sh/introduction/quick-start) 38 | - [Configuration](https://nylon.sh/core/configuration) 39 | - [Routing & Load Balancing](https://nylon.sh/core/routing) 40 | - [TLS Setup](https://nylon.sh/core/tls) 41 | - [Plugin System](https://nylon.sh/plugins/overview) 42 | - [Examples](https://nylon.sh/examples/basic-proxy) 43 | 44 | ## 🛠️ Build from Source 45 | 46 | ```sh 47 | git clone https://github.com/AssetsArt/nylon.git 48 | cd nylon 49 | make build 50 | ./target/release/nylon run -c ./examples/config.yaml 51 | ``` 52 | 53 | ## 🔗 Links 54 | 55 | - 📚 Documentation: [nylon.sh](https://nylon.sh/) 56 | - 🐛 Issues: [GitHub Issues](https://github.com/AssetsArt/nylon/issues) 57 | - 💬 Discussions: [GitHub Discussions](https://github.com/AssetsArt/nylon/discussions) 58 | 59 | ## 📄 License 60 | 61 | MIT Licensed. © AssetsArt. 62 | -------------------------------------------------------------------------------- /crates/nylon-plugin/src/plugin_manager.rs: -------------------------------------------------------------------------------- 1 | use crate::{constants::builtin_plugins, types::BuiltinPlugin}; 2 | use dashmap::DashMap; 3 | use nylon_error::NylonError; 4 | use nylon_types::plugins::FfiPlugin; 5 | use std::sync::Arc; 6 | 7 | pub struct PluginManager; 8 | impl PluginManager { 9 | pub fn try_builtin(name: &str) -> Option { 10 | // tracing::debug!("Trying builtin plugin: {}", name); 11 | match name { 12 | builtin_plugins::REQUEST_HEADER_MODIFIER => Some(BuiltinPlugin::RequestHeaderModifier), 13 | builtin_plugins::RESPONSE_HEADER_MODIFIER => { 14 | Some(BuiltinPlugin::ResponseHeaderModifier) 15 | } 16 | _ => None, 17 | } 18 | } 19 | 20 | pub fn is_request_filter(name: &str) -> bool { 21 | matches!(name, builtin_plugins::REQUEST_HEADER_MODIFIER) 22 | } 23 | 24 | pub fn is_response_filter(name: &str) -> bool { 25 | matches!(name, builtin_plugins::RESPONSE_HEADER_MODIFIER) 26 | } 27 | 28 | pub fn get_plugin(name: &str) -> Result, NylonError> { 29 | let Some(plugins) = 30 | &nylon_store::get::>>(nylon_store::KEY_PLUGINS) 31 | else { 32 | return Err(NylonError::ConfigError("Plugins not found".to_string())); 33 | }; 34 | 35 | let Some(plugin) = plugins.get(name) else { 36 | return Err(NylonError::ConfigError(format!( 37 | "Plugin '{}' not found", 38 | name 39 | ))); 40 | }; 41 | 42 | Ok(plugin.clone()) 43 | } 44 | 45 | // /// Get or create a session stream for a plugin 46 | // pub fn get_or_create_session_stream( 47 | // plugin_name: &str, 48 | // ctx: &mut NylonContext, 49 | // ) -> Result { 50 | // let plugin = Self::get_plugin(plugin_name)?; 51 | 52 | // Ok(ctx 53 | // .session_stream 54 | // .entry(plugin_name.to_string()) 55 | // .or_insert_with(|| SessionStream::new(plugin)) 56 | // .clone()) 57 | // } 58 | } 59 | -------------------------------------------------------------------------------- /sdk/go/fbs/nylon_plugin/HeaderKeyValue.go: -------------------------------------------------------------------------------- 1 | // Code generated by the FlatBuffers compiler. DO NOT EDIT. 2 | 3 | package nylon_plugin 4 | 5 | import ( 6 | flatbuffers "github.com/google/flatbuffers/go" 7 | ) 8 | 9 | type HeaderKeyValue struct { 10 | _tab flatbuffers.Table 11 | } 12 | 13 | func GetRootAsHeaderKeyValue(buf []byte, offset flatbuffers.UOffsetT) *HeaderKeyValue { 14 | n := flatbuffers.GetUOffsetT(buf[offset:]) 15 | x := &HeaderKeyValue{} 16 | x.Init(buf, n+offset) 17 | return x 18 | } 19 | 20 | func FinishHeaderKeyValueBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { 21 | builder.Finish(offset) 22 | } 23 | 24 | func GetSizePrefixedRootAsHeaderKeyValue(buf []byte, offset flatbuffers.UOffsetT) *HeaderKeyValue { 25 | n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) 26 | x := &HeaderKeyValue{} 27 | x.Init(buf, n+offset+flatbuffers.SizeUint32) 28 | return x 29 | } 30 | 31 | func FinishSizePrefixedHeaderKeyValueBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { 32 | builder.FinishSizePrefixed(offset) 33 | } 34 | 35 | func (rcv *HeaderKeyValue) Init(buf []byte, i flatbuffers.UOffsetT) { 36 | rcv._tab.Bytes = buf 37 | rcv._tab.Pos = i 38 | } 39 | 40 | func (rcv *HeaderKeyValue) Table() flatbuffers.Table { 41 | return rcv._tab 42 | } 43 | 44 | func (rcv *HeaderKeyValue) Key() []byte { 45 | o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) 46 | if o != 0 { 47 | return rcv._tab.ByteVector(o + rcv._tab.Pos) 48 | } 49 | return nil 50 | } 51 | 52 | func (rcv *HeaderKeyValue) Value() []byte { 53 | o := flatbuffers.UOffsetT(rcv._tab.Offset(6)) 54 | if o != 0 { 55 | return rcv._tab.ByteVector(o + rcv._tab.Pos) 56 | } 57 | return nil 58 | } 59 | 60 | func HeaderKeyValueStart(builder *flatbuffers.Builder) { 61 | builder.StartObject(2) 62 | } 63 | func HeaderKeyValueAddKey(builder *flatbuffers.Builder, key flatbuffers.UOffsetT) { 64 | builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(key), 0) 65 | } 66 | func HeaderKeyValueAddValue(builder *flatbuffers.Builder, value flatbuffers.UOffsetT) { 67 | builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(value), 0) 68 | } 69 | func HeaderKeyValueEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { 70 | return builder.EndObject() 71 | } 72 | -------------------------------------------------------------------------------- /sdk/go/fbs/nylon_plugin/NylonHttpHeaders.go: -------------------------------------------------------------------------------- 1 | // Code generated by the FlatBuffers compiler. DO NOT EDIT. 2 | 3 | package nylon_plugin 4 | 5 | import ( 6 | flatbuffers "github.com/google/flatbuffers/go" 7 | ) 8 | 9 | type NylonHttpHeaders struct { 10 | _tab flatbuffers.Table 11 | } 12 | 13 | func GetRootAsNylonHttpHeaders(buf []byte, offset flatbuffers.UOffsetT) *NylonHttpHeaders { 14 | n := flatbuffers.GetUOffsetT(buf[offset:]) 15 | x := &NylonHttpHeaders{} 16 | x.Init(buf, n+offset) 17 | return x 18 | } 19 | 20 | func FinishNylonHttpHeadersBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { 21 | builder.Finish(offset) 22 | } 23 | 24 | func GetSizePrefixedRootAsNylonHttpHeaders(buf []byte, offset flatbuffers.UOffsetT) *NylonHttpHeaders { 25 | n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) 26 | x := &NylonHttpHeaders{} 27 | x.Init(buf, n+offset+flatbuffers.SizeUint32) 28 | return x 29 | } 30 | 31 | func FinishSizePrefixedNylonHttpHeadersBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { 32 | builder.FinishSizePrefixed(offset) 33 | } 34 | 35 | func (rcv *NylonHttpHeaders) Init(buf []byte, i flatbuffers.UOffsetT) { 36 | rcv._tab.Bytes = buf 37 | rcv._tab.Pos = i 38 | } 39 | 40 | func (rcv *NylonHttpHeaders) Table() flatbuffers.Table { 41 | return rcv._tab 42 | } 43 | 44 | func (rcv *NylonHttpHeaders) Headers(obj *HeaderKeyValue, j int) bool { 45 | o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) 46 | if o != 0 { 47 | x := rcv._tab.Vector(o) 48 | x += flatbuffers.UOffsetT(j) * 4 49 | x = rcv._tab.Indirect(x) 50 | obj.Init(rcv._tab.Bytes, x) 51 | return true 52 | } 53 | return false 54 | } 55 | 56 | func (rcv *NylonHttpHeaders) HeadersLength() int { 57 | o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) 58 | if o != 0 { 59 | return rcv._tab.VectorLen(o) 60 | } 61 | return 0 62 | } 63 | 64 | func NylonHttpHeadersStart(builder *flatbuffers.Builder) { 65 | builder.StartObject(1) 66 | } 67 | func NylonHttpHeadersAddHeaders(builder *flatbuffers.Builder, headers flatbuffers.UOffsetT) { 68 | builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(headers), 0) 69 | } 70 | func NylonHttpHeadersStartHeadersVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT { 71 | return builder.StartVector(4, numElems, 4) 72 | } 73 | func NylonHttpHeadersEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { 74 | return builder.EndObject() 75 | } 76 | -------------------------------------------------------------------------------- /crates/nylon-types/src/plugins.rs: -------------------------------------------------------------------------------- 1 | use libloading::{Library, Symbol}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::sync::Arc; 4 | 5 | #[derive(Debug, Clone)] 6 | pub enum PluginPhase { 7 | Zero, 8 | RequestFilter, 9 | ResponseFilter, 10 | ResponseBodyFilter, 11 | Logging, 12 | } 13 | 14 | impl PluginPhase { 15 | pub fn to_u8(self) -> u8 { 16 | match self { 17 | PluginPhase::Zero => 0, 18 | PluginPhase::RequestFilter => 1, 19 | PluginPhase::ResponseFilter => 2, 20 | PluginPhase::ResponseBodyFilter => 3, 21 | PluginPhase::Logging => 4, 22 | } 23 | } 24 | } 25 | 26 | #[repr(C)] 27 | pub struct FfiBuffer { 28 | pub sid: u32, 29 | pub phase: u8, 30 | pub method: u32, 31 | pub ptr: *const u8, 32 | pub len: u64, 33 | } 34 | 35 | #[derive(Debug, Deserialize, Serialize, Clone)] 36 | pub enum PluginType { 37 | #[serde(rename = "wasm")] 38 | Wasm, 39 | #[serde(rename = "ffi")] 40 | Ffi, 41 | } 42 | 43 | #[derive(Debug, Deserialize, Serialize, Clone)] 44 | pub struct LifeCycle { 45 | pub setup: Option, 46 | pub shutdown: Option, 47 | } 48 | 49 | #[derive(Debug, Deserialize, Serialize, Clone)] 50 | pub struct PluginItem { 51 | pub name: String, 52 | pub file: String, 53 | #[serde(rename = "type")] 54 | pub plugin_type: PluginType, 55 | pub entry: Option>, 56 | pub config: Option, 57 | } 58 | 59 | // FFI Plugin 60 | pub type FfiInitializeFn = unsafe extern "C" fn(*const u8, u32); 61 | pub type FfiPluginFreeFn = unsafe extern "C" fn(*mut u8); 62 | pub type FfiRegisterSessionFn = 63 | unsafe extern "C" fn(u32, *const u8, u32, extern "C" fn(*const FfiBuffer)) -> bool; 64 | pub type FfiEventStreamFn = unsafe extern "C" fn(*const FfiBuffer); 65 | pub type FfiCloseSessionFn = unsafe extern "C" fn(u32); 66 | pub type FfiShutdownFn = unsafe extern "C" fn(); 67 | 68 | #[derive(Debug)] 69 | pub struct FfiPlugin { 70 | pub _lib: Arc, 71 | pub plugin_free: Symbol<'static, FfiPluginFreeFn>, 72 | pub register_session: Symbol<'static, FfiRegisterSessionFn>, 73 | pub event_stream: Symbol<'static, FfiEventStreamFn>, 74 | pub close_session: Symbol<'static, FfiCloseSessionFn>, 75 | pub shutdown: Symbol<'static, FfiShutdownFn>, 76 | } 77 | 78 | // Plugin Session Stream 79 | #[derive(Debug, Clone)] 80 | pub struct SessionStream { 81 | pub plugin: Arc, 82 | pub session_id: u32, 83 | } 84 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Customize default theme styling by overriding CSS variables: 3 | * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css 4 | */ 5 | 6 | /** 7 | * Colors 8 | * -------------------------------------------------------------------------- */ 9 | 10 | :root { 11 | --vp-c-brand-1: #646cff; 12 | --vp-c-brand-2: #747bff; 13 | --vp-c-brand-3: #535bf2; 14 | --vp-c-brand-soft: rgba(100, 108, 255, 0.14); 15 | } 16 | 17 | .dark { 18 | --vp-c-brand-1: #a8b1ff; 19 | --vp-c-brand-2: #8890ff; 20 | --vp-c-brand-3: #6873ff; 21 | --vp-c-brand-soft: rgba(100, 108, 255, 0.16); 22 | } 23 | 24 | /** 25 | * Component: Button 26 | * -------------------------------------------------------------------------- */ 27 | 28 | :root { 29 | --vp-button-brand-border: transparent; 30 | --vp-button-brand-text: var(--vp-c-white); 31 | --vp-button-brand-bg: var(--vp-c-brand-3); 32 | --vp-button-brand-hover-border: transparent; 33 | --vp-button-brand-hover-text: var(--vp-c-white); 34 | --vp-button-brand-hover-bg: var(--vp-c-brand-2); 35 | --vp-button-brand-active-border: transparent; 36 | --vp-button-brand-active-text: var(--vp-c-white); 37 | --vp-button-brand-active-bg: var(--vp-c-brand-1); 38 | } 39 | 40 | /** 41 | * Component: Home 42 | * -------------------------------------------------------------------------- */ 43 | 44 | :root { 45 | --vp-home-hero-name-color: transparent; 46 | --vp-home-hero-name-background: -webkit-linear-gradient( 47 | 120deg, 48 | #bd34fe 30%, 49 | #41d1ff 50 | ); 51 | 52 | --vp-home-hero-image-background-image: linear-gradient( 53 | -45deg, 54 | #bd34fe 50%, 55 | #47caff 50% 56 | ); 57 | --vp-home-hero-image-filter: blur(44px); 58 | } 59 | 60 | @media (min-width: 640px) { 61 | :root { 62 | --vp-home-hero-image-filter: blur(56px); 63 | } 64 | } 65 | 66 | @media (min-width: 960px) { 67 | :root { 68 | --vp-home-hero-image-filter: blur(68px); 69 | } 70 | } 71 | 72 | /** 73 | * Component: Custom Block 74 | * -------------------------------------------------------------------------- */ 75 | 76 | :root { 77 | --vp-custom-block-tip-border: transparent; 78 | --vp-custom-block-tip-text: var(--vp-c-text-1); 79 | --vp-custom-block-tip-bg: var(--vp-c-brand-soft); 80 | --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft); 81 | } 82 | 83 | /** 84 | * Component: Algolia 85 | * -------------------------------------------------------------------------- */ 86 | 87 | .DocSearch { 88 | --docsearch-primary-color: var(--vp-c-brand-1) !important; 89 | } 90 | 91 | -------------------------------------------------------------------------------- /examples/proxy/base.yaml: -------------------------------------------------------------------------------- 1 | header_selector: x-nylon-proxy 2 | 3 | plugins: 4 | - name: plugin_sdk 5 | type: ffi 6 | file: ./target/examples/go/plugin_sdk.so 7 | config: 8 | debug: true 9 | 10 | services: 11 | # Http service 12 | - name: app-service 13 | service_type: http 14 | algorithm: round_robin # Options: round_robin, random, consistent, weighted 15 | endpoints: 16 | - ip: 127.0.0.1 17 | port: 3001 18 | - ip: 127.0.0.1 19 | port: 3002 20 | health_check: 21 | enabled: false 22 | path: /health 23 | interval: 3s 24 | timeout: 1s 25 | healthy_threshold: 2 26 | unhealthy_threshold: 2 27 | 28 | # WebSocket and streaming via plugin 29 | - name: ws-service 30 | service_type: plugin 31 | plugin: 32 | name: plugin_sdk 33 | entry: ws 34 | 35 | - name: stream-service 36 | service_type: plugin 37 | plugin: 38 | name: plugin_sdk 39 | entry: stream 40 | 41 | # Static assets / SPA 42 | - name: static 43 | service_type: static 44 | static: 45 | root: ./examples/static 46 | index: index.html 47 | spa: true 48 | 49 | - name: myapp-service 50 | service_type: plugin 51 | plugin: 52 | name: plugin_sdk 53 | entry: myapp 54 | 55 | middleware_groups: 56 | # Security & request metadata headers applied to most routes 57 | security: 58 | - plugin: RequestHeaderModifier 59 | payload: 60 | remove: 61 | - x-version 62 | set: 63 | - name: x-request-id 64 | value: "${uuid(v7)}" 65 | - name: x-forwarded-for 66 | value: "${request(client_ip)}" 67 | - name: x-host 68 | value: "${header(host)}" 69 | - name: x-timestamp 70 | value: "${timestamp()}" 71 | - plugin: ResponseHeaderModifier 72 | payload: 73 | set: 74 | - name: cache-control 75 | value: "no-store" 76 | - name: referrer-policy 77 | value: "no-referrer" 78 | - name: x-content-type-options 79 | value: "nosniff" 80 | - name: x-frame-options 81 | value: "DENY" 82 | - name: content-security-policy 83 | value: "default-src 'self'" 84 | - name: x-server 85 | value: ${or(env(SERVER_NAME), 'nylon-demo')} 86 | 87 | # Authorization example using FFI plugin 88 | auth: 89 | - plugin: plugin_sdk 90 | entry: "authz" 91 | payload: 92 | client_ip: "${request(client_ip)}" 93 | user_id: "${header(x-user-id)}" 94 | -------------------------------------------------------------------------------- /.github/workflows/Ci.yml: -------------------------------------------------------------------------------- 1 | name: Build, Publish Docker Image, and Release Binaries 2 | 3 | on: 4 | push: 5 | branches: 6 | - release 7 | 8 | jobs: 9 | build-and-release-binaries: 10 | permissions: write-all 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Rust 17 | uses: actions-rust-lang/setup-rust-toolchain@v1 18 | with: 19 | toolchain: stable 20 | 21 | - name: Install cross 22 | run: cargo install cross --git https://github.com/cross-rs/cross 23 | 24 | - name: Install jq 25 | run: sudo apt-get install -y jq 26 | 27 | - name: Extract version from Cargo.toml 28 | id: extract_version 29 | run: | 30 | VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "nylon") | .version') 31 | echo "version=$VERSION" >> $GITHUB_OUTPUT 32 | 33 | - name: Build Linux binary x86_64-gnu 34 | run: | 35 | cross build --target x86_64-unknown-linux-gnu --release 36 | mkdir -p build/linux/ 37 | cp target/x86_64-unknown-linux-gnu/release/nylon build/linux/nylon-x86_64-linux-gnu 38 | 39 | - name: Build Linux binary x86_64-musl 40 | run: | 41 | cross build --target x86_64-unknown-linux-musl --release 42 | mkdir -p build/linux/ 43 | cp target/x86_64-unknown-linux-musl/release/nylon build/linux/nylon-x86_64-linux-musl 44 | 45 | - name: Build Linux binary aarch64-gnu 46 | run: | 47 | cross build --target aarch64-unknown-linux-gnu --release 48 | mkdir -p build/linux/ 49 | cp target/aarch64-unknown-linux-gnu/release/nylon build/linux/nylon-aarch64-linux-gnu 50 | 51 | - name: Build Linux binary aarch64-musl 52 | run: | 53 | cross build --target aarch64-unknown-linux-musl --release 54 | mkdir -p build/linux/ 55 | cp target/aarch64-unknown-linux-musl/release/nylon build/linux/nylon-aarch64-linux-musl 56 | 57 | - name: Generate Checksums 58 | run: | 59 | cd build/linux 60 | shasum -a 256 * > linux-checksums.txt 61 | 62 | - name: Push binaries to release 63 | uses: softprops/action-gh-release@v1 64 | with: 65 | repository: ${{ github.repository }} 66 | files: | 67 | build/linux/nylon-x86_64-linux-gnu 68 | build/linux/nylon-x86_64-linux-musl 69 | build/linux/nylon-aarch64-linux-gnu 70 | build/linux/nylon-aarch64-linux-musl 71 | build/linux/linux-checksums.txt 72 | tag_name: v${{ steps.extract_version.outputs.version }} 73 | name: v${{ steps.extract_version.outputs.version }} 74 | token: ${{ secrets.GITHUB_TOKEN }} 75 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | export default defineConfig({ 4 | title: 'Nylon', 5 | description: 'High-performance HTTP/HTTPS reverse proxy with plugin support', 6 | 7 | head: [ 8 | ['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }], 9 | ], 10 | 11 | themeConfig: { 12 | logo: '/logo.svg', 13 | 14 | nav: [ 15 | { text: 'Guide', link: '/introduction/what-is-nylon' }, 16 | { text: 'Plugins', link: '/plugins/overview' }, 17 | { text: 'API', link: '/api/configuration' }, 18 | { text: 'Examples', link: '/examples/basic-proxy' }, 19 | ], 20 | 21 | sidebar: { 22 | '/': [ 23 | { 24 | text: 'Introduction', 25 | items: [ 26 | { text: 'What is Nylon?', link: '/introduction/what-is-nylon' }, 27 | { text: 'Installation', link: '/introduction/installation' }, 28 | { text: 'Quick Start', link: '/introduction/quick-start' }, 29 | ] 30 | }, 31 | { 32 | text: 'Core Concepts', 33 | items: [ 34 | { text: 'Configuration', link: '/core/configuration' }, 35 | { text: 'Routing', link: '/core/routing' }, 36 | { text: 'Load Balancing', link: '/core/load-balancing' }, 37 | { text: 'TLS/HTTPS', link: '/core/tls' }, 38 | { text: 'Middleware', link: '/core/middleware' }, 39 | ] 40 | }, 41 | { 42 | text: 'Plugin Development', 43 | items: [ 44 | { text: 'Overview', link: '/plugins/overview' }, 45 | { text: 'Plugin Phases', link: '/plugins/phases' }, 46 | { text: 'Go SDK', link: '/plugins/go-sdk' }, 47 | { text: 'Request Handling', link: '/plugins/request' }, 48 | { text: 'Response Handling', link: '/plugins/response' }, 49 | { text: 'WebSocket Support', link: '/plugins/websocket' }, 50 | ] 51 | }, 52 | { 53 | text: 'Examples', 54 | items: [ 55 | { text: 'Basic Proxy', link: '/examples/basic-proxy' }, 56 | { text: 'Authentication', link: '/examples/authentication' }, 57 | { text: 'Rate Limiting', link: '/examples/rate-limiting' }, 58 | { text: 'Custom Headers', link: '/examples/custom-headers' }, 59 | { text: 'WebSocket Proxy', link: '/examples/websocket' }, 60 | ] 61 | }, 62 | { 63 | text: 'API Reference', 64 | items: [ 65 | { text: 'Configuration Schema', link: '/api/configuration' }, 66 | { text: 'Go SDK API', link: '/api/go-sdk' }, 67 | ] 68 | } 69 | ] 70 | }, 71 | 72 | socialLinks: [ 73 | { icon: 'github', link: 'https://github.com/AssetsArt/nylon' } 74 | ], 75 | 76 | footer: { 77 | message: 'Released under the MIT License.', 78 | copyright: 'Copyright © 2025-present' 79 | }, 80 | 81 | search: { 82 | provider: 'local' 83 | } 84 | } 85 | }) 86 | 87 | -------------------------------------------------------------------------------- /crates/nylon-plugin/src/constants.rs: -------------------------------------------------------------------------------- 1 | //! Constants used throughout the plugin system 2 | 3 | // Plugin method constants 4 | pub mod methods { 5 | // Control methods 6 | pub const NEXT: u32 = 1; 7 | pub const END: u32 = 2; 8 | pub const GET_PAYLOAD: u32 = 3; 9 | 10 | // Response methods 11 | pub const SET_RESPONSE_HEADER: u32 = 100; 12 | pub const REMOVE_RESPONSE_HEADER: u32 = 101; 13 | pub const SET_RESPONSE_STATUS: u32 = 102; 14 | pub const SET_RESPONSE_FULL_BODY: u32 = 103; 15 | pub const SET_RESPONSE_STREAM_DATA: u32 = 104; 16 | pub const SET_RESPONSE_STREAM_END: u32 = 105; 17 | pub const SET_RESPONSE_STREAM_HEADER: u32 = 106; 18 | pub const READ_RESPONSE_FULL_BODY: u32 = 107; 19 | pub const READ_RESPONSE_STATUS: u32 = 108; 20 | pub const READ_RESPONSE_BYTES: u32 = 109; 21 | pub const READ_RESPONSE_HEADERS: u32 = 110; 22 | pub const READ_RESPONSE_DURATION: u32 = 111; 23 | pub const READ_RESPONSE_ERROR: u32 = 112; 24 | 25 | // Request methods 26 | pub const READ_REQUEST_FULL_BODY: u32 = 200; 27 | pub const READ_REQUEST_HEADER: u32 = 201; 28 | pub const READ_REQUEST_HEADERS: u32 = 202; 29 | pub const READ_REQUEST_URL: u32 = 203; 30 | pub const READ_REQUEST_PATH: u32 = 204; 31 | pub const READ_REQUEST_QUERY: u32 = 205; 32 | pub const READ_REQUEST_PARAMS: u32 = 206; 33 | pub const READ_REQUEST_HOST: u32 = 207; 34 | pub const READ_REQUEST_CLIENT_IP: u32 = 208; 35 | pub const READ_REQUEST_METHOD: u32 = 209; 36 | pub const READ_REQUEST_BYTES: u32 = 210; 37 | pub const READ_REQUEST_TIMESTAMP: u32 = 211; 38 | 39 | // WebSocket methods (Plugin -> Rust) 40 | pub const WEBSOCKET_UPGRADE: u32 = 300; 41 | pub const WEBSOCKET_SEND_TEXT: u32 = 301; 42 | pub const WEBSOCKET_SEND_BINARY: u32 = 302; 43 | pub const WEBSOCKET_CLOSE: u32 = 303; 44 | 45 | // WebSocket room methods (Plugin -> Rust) 46 | pub const WEBSOCKET_JOIN_ROOM: u32 = 310; 47 | pub const WEBSOCKET_LEAVE_ROOM: u32 = 311; 48 | pub const WEBSOCKET_BROADCAST_ROOM_TEXT: u32 = 312; 49 | pub const WEBSOCKET_BROADCAST_ROOM_BINARY: u32 = 313; 50 | 51 | // WebSocket events (Rust -> Plugin) 52 | pub const WEBSOCKET_ON_OPEN: u32 = 350; 53 | pub const WEBSOCKET_ON_MESSAGE_TEXT: u32 = 351; 54 | pub const WEBSOCKET_ON_MESSAGE_BINARY: u32 = 352; 55 | pub const WEBSOCKET_ON_CLOSE: u32 = 353; 56 | pub const WEBSOCKET_ON_ERROR: u32 = 354; 57 | } 58 | 59 | // FFI symbol names 60 | pub mod ffi_symbols { 61 | pub const INITIALIZE: &str = "initialize"; 62 | pub const PLUGIN_FREE: &str = "plugin_free"; 63 | pub const REGISTER_SESSION: &str = "register_session_stream"; 64 | pub const EVENT_STREAM: &str = "event_stream"; 65 | pub const CLOSE_SESSION: &str = "close_session_stream"; 66 | pub const SHUTDOWN: &str = "shutdown"; 67 | } 68 | 69 | // Builtin plugin names 70 | pub mod builtin_plugins { 71 | pub const REQUEST_HEADER_MODIFIER: &str = "RequestHeaderModifier"; 72 | pub const RESPONSE_HEADER_MODIFIER: &str = "ResponseHeaderModifier"; 73 | } 74 | -------------------------------------------------------------------------------- /crates/nylon/src/dynamic_certificate.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use nylon_error::NylonError; 3 | use nylon_store::{routes, tls}; 4 | use openssl::{ 5 | pkey::PKey, 6 | ssl::{NameType, SslRef}, 7 | x509::X509, 8 | }; 9 | use pingora::{ 10 | listeners::{TlsAccept, tls::TlsSettings}, 11 | tls::ext, 12 | }; 13 | use tracing::error; 14 | 15 | #[derive(Default)] 16 | pub struct DynamicCertificate; 17 | 18 | impl DynamicCertificate { 19 | pub fn new() -> Self { 20 | Self 21 | } 22 | } 23 | 24 | pub fn new_tls_settings() -> Result { 25 | let mut tls = TlsSettings::with_callbacks(Box::new(DynamicCertificate::new())) 26 | .map_err(|e| NylonError::PingoraError(e.to_string()))?; 27 | tls.enable_h2(); 28 | Ok(tls) 29 | } 30 | 31 | #[async_trait] 32 | impl TlsAccept for DynamicCertificate { 33 | async fn certificate_callback(&self, ssl: &mut SslRef) { 34 | let server_name = ssl.servername(NameType::HOST_NAME); 35 | 36 | let server_name = match server_name { 37 | Some(s) => s, 38 | None => { 39 | error!("Unable to get server name"); 40 | "localhost" 41 | } 42 | }; 43 | 44 | // Enabled tls route? 45 | if let Err(e) = routes::get_tls_route(server_name) { 46 | error!("Unable to get TLS route: {}", e); 47 | return; 48 | } 49 | // debug!("server_name: {}", server_name); 50 | let tls_store = match tls::get_certs(server_name) { 51 | Ok(tls_store) => tls_store, 52 | Err(e) => { 53 | error!("Unable to get TLS store: {}", e); 54 | return; 55 | } 56 | }; 57 | // debug!("tls_store: {:?}", tls_store); 58 | let cert = match X509::from_pem(&tls_store.cert) { 59 | Ok(cert) => cert, 60 | Err(e) => { 61 | error!("Failed to parse certificate: {}", e); 62 | return; 63 | } 64 | }; 65 | let key = match PKey::private_key_from_pem(&tls_store.key) { 66 | Ok(key) => key, 67 | Err(e) => { 68 | error!("Failed to parse private key: {}", e); 69 | return; 70 | } 71 | }; 72 | 73 | if let Err(e) = ext::ssl_use_certificate(ssl, &cert) { 74 | error!("Failed to use certificate: {}", e); 75 | } 76 | 77 | if let Err(e) = ext::ssl_use_private_key(ssl, &key) { 78 | error!("Failed to use private key: {}", e); 79 | } 80 | 81 | for chain in &tls_store.chain { 82 | let chain = match X509::from_pem(chain) { 83 | Ok(chain) => chain, 84 | Err(e) => { 85 | error!("Failed to parse chain certificate: {}", e); 86 | return; 87 | } 88 | }; 89 | if let Err(e) = ext::ssl_add_chain_cert(ssl, &chain) { 90 | error!("Failed to add chain certificate: {}", e); 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /crates/nylon-plugin/src/native/header_modifier.rs: -------------------------------------------------------------------------------- 1 | use nylon_error::NylonError; 2 | use nylon_types::{ 3 | context::NylonContext, 4 | template::{Expr, apply_payload_ast}, 5 | }; 6 | use pingora::proxy::Session; 7 | use serde::Deserialize; 8 | use serde_json::Value; 9 | use std::collections::HashMap; 10 | 11 | /// Payload structure for header modification 12 | #[derive(Debug, Deserialize, Clone)] 13 | struct Payload { 14 | remove: Option>, 15 | set: Option>, 16 | } 17 | 18 | /// Header structure for setting headers 19 | #[derive(Debug, Deserialize, Clone)] 20 | struct Header { 21 | name: String, 22 | value: String, 23 | } 24 | 25 | pub fn request( 26 | ctx: &mut NylonContext, 27 | session: &mut Session, 28 | payload: &Option, 29 | payload_ast: &Option>>, 30 | ) -> Result<(), NylonError> { 31 | let headers = session.req_header_mut(); 32 | let payload = match payload.as_ref() { 33 | Some(payload) => { 34 | let mut payload = payload.clone(); 35 | if let Some(payload_ast) = payload_ast { 36 | apply_payload_ast(&mut payload, payload_ast, headers, ctx); 37 | } 38 | serde_json::from_value::(payload.clone()) 39 | .map_err(|e| NylonError::ConfigError(e.to_string()))? 40 | } 41 | None => Payload { 42 | remove: None, 43 | set: None, 44 | }, 45 | }; 46 | // println!("payload: {:#?}", payload); 47 | if let Some(set) = payload.set { 48 | for header in set { 49 | let _ = headers.remove_header(&header.name); 50 | let name = header.name.to_ascii_lowercase(); 51 | let _ = headers.append_header(name.clone(), &header.value); 52 | } 53 | } 54 | if let Some(remove) = payload.remove { 55 | for header in remove { 56 | let _ = headers.remove_header(&header.to_ascii_lowercase()); 57 | } 58 | } 59 | Ok(()) 60 | } 61 | 62 | pub fn response( 63 | ctx: &mut NylonContext, 64 | session: &mut Session, 65 | payload: &Option, 66 | payload_ast: &Option>>, 67 | ) -> Result<(), NylonError> { 68 | let headers = session.req_header(); 69 | let payload = match payload.as_ref() { 70 | Some(payload) => { 71 | let mut payload = payload.clone(); 72 | if let Some(payload_ast) = payload_ast { 73 | apply_payload_ast(&mut payload, payload_ast, headers, ctx); 74 | } 75 | serde_json::from_value::(payload.clone()) 76 | .map_err(|e| NylonError::ConfigError(e.to_string()))? 77 | } 78 | None => Payload { 79 | remove: None, 80 | set: None, 81 | }, 82 | }; 83 | if let Some(set) = payload.set { 84 | let mut map = ctx.add_response_header.write().expect("lock"); 85 | for header in set { 86 | let _ = map.insert(header.name, header.value); 87 | } 88 | } 89 | if let Some(remove) = payload.remove { 90 | let mut vec = ctx.remove_response_header.write().expect("lock"); 91 | for header in remove { 92 | vec.push(header); 93 | } 94 | } 95 | Ok(()) 96 | } 97 | -------------------------------------------------------------------------------- /crates/nylon-types/src/websocket.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::HashMap; 3 | use tokio::sync::mpsc; 4 | 5 | /// WebSocket adapter configuration 6 | #[derive(Debug, Clone, Serialize, Deserialize)] 7 | pub struct WebSocketAdapterConfig { 8 | pub adapter_type: AdapterType, 9 | pub redis: Option, 10 | pub cluster: Option, 11 | } 12 | 13 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 14 | pub enum AdapterType { 15 | #[serde(rename = "memory")] 16 | Memory, 17 | #[serde(rename = "redis")] 18 | Redis, 19 | #[serde(rename = "cluster")] 20 | Cluster, 21 | } 22 | 23 | #[derive(Debug, Clone, Serialize, Deserialize)] 24 | pub struct RedisAdapterConfig { 25 | pub host: String, 26 | pub port: u16, 27 | pub password: Option, 28 | pub db: Option, 29 | pub key_prefix: Option, 30 | } 31 | 32 | #[derive(Debug, Clone, Serialize, Deserialize)] 33 | pub struct ClusterAdapterConfig { 34 | pub nodes: Vec, 35 | pub key_prefix: Option, 36 | } 37 | 38 | /// WebSocket connection information 39 | #[derive(Debug, Clone, Serialize, Deserialize)] 40 | pub struct WebSocketConnection { 41 | pub id: String, 42 | pub session_id: u32, 43 | pub rooms: Vec, 44 | pub node_id: String, 45 | pub connected_at: u64, 46 | pub metadata: HashMap, 47 | } 48 | 49 | /// WebSocket room information 50 | #[derive(Debug, Clone, Serialize, Deserialize)] 51 | pub struct WebSocketRoom { 52 | pub name: String, 53 | pub connections: Vec, 54 | pub created_at: u64, 55 | pub metadata: HashMap, 56 | } 57 | 58 | /// WebSocket event types for cluster communication 59 | #[derive(Debug, Clone, Serialize, Deserialize)] 60 | pub enum WebSocketEvent { 61 | /// Connection joined a room 62 | JoinRoom { 63 | connection_id: String, 64 | room: String, 65 | node_id: String, 66 | }, 67 | /// Connection left a room 68 | LeaveRoom { 69 | connection_id: String, 70 | room: String, 71 | node_id: String, 72 | }, 73 | /// Connection disconnected 74 | Disconnect { 75 | connection_id: String, 76 | node_id: String, 77 | }, 78 | /// Broadcast message to room 79 | BroadcastToRoom { 80 | room: String, 81 | message: WebSocketMessage, 82 | exclude_connection: Option, 83 | sender_node_id: String, 84 | }, 85 | /// Send message to specific connection 86 | SendToConnection { 87 | connection_id: String, 88 | message: WebSocketMessage, 89 | sender_node_id: String, 90 | }, 91 | } 92 | 93 | /// WebSocket message types 94 | #[derive(Debug, Clone, Serialize, Deserialize)] 95 | pub enum WebSocketMessage { 96 | Text(String), 97 | Binary(Vec), 98 | Close { code: u16, reason: String }, 99 | Ping(Vec), 100 | Pong(Vec), 101 | } 102 | 103 | /// Adapter event sender type 104 | pub type AdapterEventSender = mpsc::UnboundedSender; 105 | 106 | /// Adapter event receiver type 107 | pub type AdapterEventReceiver = mpsc::UnboundedReceiver; 108 | -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /crates/nylon-tls/src/certificate.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, NaiveDateTime, Utc}; 2 | use nylon_error::NylonError; 3 | use openssl::asn1::Asn1TimeRef; 4 | use openssl::x509::X509; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | /// ข้อมูล certificate พร้อมวันหมดอายุ 8 | #[derive(Debug, Clone, Serialize, Deserialize)] 9 | pub struct CertificateInfo { 10 | pub domain: String, 11 | pub cert: Vec, 12 | pub key: Vec, 13 | pub chain: Vec>, 14 | pub expires_at: DateTime, 15 | pub issued_at: DateTime, 16 | } 17 | 18 | impl CertificateInfo { 19 | /// สร้าง CertificateInfo จาก certificate และ key 20 | pub fn new( 21 | domain: String, 22 | cert: Vec, 23 | key: Vec, 24 | chain: Vec>, 25 | ) -> Result { 26 | let x509 = X509::from_pem(&cert) 27 | .map_err(|e| NylonError::ConfigError(format!("Failed to parse certificate: {}", e)))?; 28 | 29 | let not_after = x509.not_after(); 30 | let not_before = x509.not_before(); 31 | 32 | // แปลง ASN1Time เป็น DateTime 33 | let expires_at = asn1_time_to_datetime(not_after)?; 34 | let issued_at = asn1_time_to_datetime(not_before)?; 35 | 36 | Ok(Self { 37 | domain, 38 | cert, 39 | key, 40 | chain, 41 | expires_at, 42 | issued_at, 43 | }) 44 | } 45 | 46 | /// ตรวจสอบว่า certificate ใกล้หมดอายุหรือไม่ (น้อยกว่า 30 วัน) 47 | pub fn needs_renewal(&self) -> bool { 48 | let now = Utc::now(); 49 | let days_until_expiry = (self.expires_at - now).num_days(); 50 | days_until_expiry < 30 51 | } 52 | 53 | /// ตรวจสอบว่า certificate หมดอายุแล้วหรือไม่ 54 | pub fn is_expired(&self) -> bool { 55 | Utc::now() > self.expires_at 56 | } 57 | 58 | /// คำนวณจำนวนวันที่เหลือจนถึงวันหมดอายุ 59 | pub fn days_until_expiry(&self) -> i64 { 60 | (self.expires_at - Utc::now()).num_days() 61 | } 62 | } 63 | 64 | /// แปลง ASN1Time เป็น DateTime โดย parse string format 65 | fn asn1_time_to_datetime(asn1_time: &Asn1TimeRef) -> Result, NylonError> { 66 | // ASN1Time format examples: 67 | // "Oct 5 10:02:11 2025 GMT" (2 spaces for single digit day) 68 | // "Oct 15 10:02:11 2025 GMT" (1 space for double digit day) 69 | let time_str = asn1_time.to_string(); 70 | 71 | // Try parsing with various formats 72 | // Format 1: "Oct 5 10:02:11 2025 GMT" (with GMT suffix) 73 | if let Ok(naive) = NaiveDateTime::parse_from_str(&time_str, "%b %e %H:%M:%S %Y GMT") { 74 | return Ok(DateTime::::from_naive_utc_and_offset(naive, Utc)); 75 | } 76 | 77 | // Format 2: "Oct 5 10:02:11 2025" (without GMT suffix) 78 | if let Ok(naive) = NaiveDateTime::parse_from_str(&time_str, "%b %e %H:%M:%S %Y") { 79 | return Ok(DateTime::::from_naive_utc_and_offset(naive, Utc)); 80 | } 81 | 82 | // Format 3: Try with explicit timezone 83 | if let Ok(dt) = DateTime::parse_from_str(&time_str, "%b %e %H:%M:%S %Y %Z") { 84 | return Ok(dt.to_utc()); 85 | } 86 | 87 | Err(NylonError::ConfigError(format!( 88 | "Failed to parse ASN1 time: {}", 89 | time_str 90 | ))) 91 | } 92 | 93 | /// Certificate store สำหรับเก็บข้อมูล ACME certificates 94 | #[derive(Debug, Clone, Default)] 95 | pub struct CertificateStore; 96 | 97 | impl CertificateStore { 98 | pub fn new() -> Self { 99 | Self 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /crates/nylon/src/response.rs: -------------------------------------------------------------------------------- 1 | use crate::runtime::NylonRuntime; 2 | use bytes::Bytes; 3 | use nylon_types::context::NylonContext; 4 | use pingora::{ 5 | ErrorType, 6 | http::ResponseHeader, 7 | protocols::http::HttpTask, 8 | proxy::{ProxyHttp, Session}, 9 | }; 10 | use serde_json::Value; 11 | 12 | pub struct Response<'a> { 13 | pub body: Option, 14 | pub proxy: &'a NylonRuntime, 15 | pub ctx: &'a mut NylonContext, 16 | } 17 | 18 | impl<'a> Response<'a> { 19 | pub async fn new(proxy: &'a NylonRuntime, ctx: &'a mut NylonContext) -> pingora::Result { 20 | Ok(Self { 21 | body: None, 22 | proxy, 23 | ctx, 24 | }) 25 | } 26 | 27 | pub fn redirect(&mut self, redirect: String) -> &mut Self { 28 | self.status(301); 29 | { 30 | let mut headers = self.ctx.add_response_header.write().expect("lock"); 31 | headers.insert("Location".to_string(), redirect); 32 | headers.insert("Content-Length".to_string(), "0".to_string()); 33 | } 34 | self 35 | } 36 | 37 | pub fn status(&mut self, status: u16) -> &mut Self { 38 | self.ctx 39 | .set_response_status 40 | .store(status, std::sync::atomic::Ordering::Relaxed); 41 | self 42 | } 43 | 44 | pub fn body(&mut self, body: Bytes) -> &mut Self { 45 | let body_len = body.len(); 46 | self.body = Some(body); 47 | { 48 | let mut headers = self.ctx.add_response_header.write().expect("lock"); 49 | headers.insert("Content-Length".to_string(), body_len.to_string()); 50 | } 51 | self 52 | } 53 | 54 | pub fn body_json(&mut self, body: Value) -> pingora::Result<&mut Self> { 55 | let body_bytes = match serde_json::to_vec(&body) { 56 | Ok(b) => b, 57 | Err(e) => { 58 | return Err(pingora::Error::because( 59 | ErrorType::InternalError, 60 | "[Response]".to_string(), 61 | e.to_string(), 62 | )); 63 | } 64 | }; 65 | self.body(Bytes::from(body_bytes)); 66 | // self.header("Content-Type", "application/json"); 67 | Ok(self) 68 | } 69 | 70 | pub async fn send(&mut self, session: &mut Session) -> pingora::Result { 71 | let status = self 72 | .ctx 73 | .set_response_status 74 | .load(std::sync::atomic::Ordering::Relaxed); 75 | let mut headers = ResponseHeader::build(status, None)?; 76 | self.proxy 77 | .response_filter(session, &mut headers, self.ctx) 78 | .await?; 79 | let mut tasks = vec![HttpTask::Header(Box::new(headers), false)]; 80 | let _ = self 81 | .proxy 82 | .response_body_filter(session, &mut self.body, false, self.ctx) 83 | .is_ok(); 84 | if let Some(body) = self.body.clone() { 85 | tasks.push(HttpTask::Body(Some(body), false)); 86 | } else { 87 | tasks.push(HttpTask::Body(None, false)); 88 | } 89 | tasks.push(HttpTask::Done); 90 | // println!("tasks: {:?}", tasks); 91 | if let Err(e) = session.response_duplex_vec(tasks).await { 92 | tracing::error!("Error sending response: {:?}", e); 93 | return Err(pingora::Error::because( 94 | ErrorType::InternalError, 95 | "[Response]".to_string(), 96 | e.to_string(), 97 | )); 98 | } 99 | Ok(true) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Nylon Proxy Server Makefile 2 | # 3 | # This Makefile provides targets for building, developing, and managing the Nylon proxy server. 4 | # It includes development tools, build processes, and example compilation. 5 | 6 | # Configuration 7 | PORTS := 8088 8443 6192 8 | RUST_BACKTRACE := 1 9 | RUST_LOG := "info,warn,debug" 10 | DEV_CMD := cargo watch -w crates -w examples -w proto -w sdk -q -c -s "make build-examples && cargo run -- run --config ./examples/config.yaml" 11 | 12 | # Default target 13 | .PHONY: default 14 | default: dev 15 | 16 | # Development target - runs the server in development mode 17 | .PHONY: dev 18 | dev: 19 | @echo "🧹 Cleaning up existing processes on ports: $(PORTS)" 20 | @for port in $(PORTS); do \ 21 | kill -9 $$(lsof -t -i :$$port) 2>/dev/null || true; \ 22 | done 23 | @echo "🚀 Starting Nylon development server..." 24 | RUST_BACKTRACE=$(RUST_BACKTRACE) $(DEV_CMD) 25 | 26 | # Development target with debug logging 27 | .PHONY: dev-debug 28 | dev-debug: 29 | @echo "🐛 Starting Nylon development server with debug logging..." 30 | RUST_LOG=$(RUST_LOG) $(DEV_CMD) 31 | 32 | # Generate FlatBuffers code from protocol definitions 33 | .PHONY: generate 34 | generate: 35 | @echo "📝 Generating FlatBuffers code..." 36 | flatc --rust -o sdk/rust/src/fbs proto/plugin.fbs 37 | flatc --go -o sdk/go/fbs proto/plugin.fbs 38 | @echo "✅ FlatBuffers code generation completed" 39 | 40 | # Build Go examples 41 | .PHONY: build-examples 42 | build-examples: 43 | @echo "🔨 Building Go examples..." 44 | cd examples/go && go build -buildmode=c-shared -o ./../../target/examples/go/plugin_sdk.so 45 | @echo "✅ Go examples built successfully" 46 | 47 | # Build release version 48 | .PHONY: build 49 | build: 50 | @echo "🏗️ Building Nylon release version..." 51 | cargo build --release 52 | @echo "✅ Release build completed" 53 | 54 | # Clean build artifacts 55 | .PHONY: clean 56 | clean: 57 | @echo "🧹 Cleaning build artifacts..." 58 | cargo clean 59 | rm -rf target/examples/go/plugin_sdk.so 60 | @echo "✅ Clean completed" 61 | 62 | # Run tests 63 | .PHONY: test 64 | test: 65 | @echo "🧪 Running tests..." 66 | cargo test 67 | @echo "✅ Tests completed" 68 | 69 | # Check code formatting 70 | .PHONY: fmt 71 | fmt: 72 | @echo "🎨 Checking code formatting..." 73 | cargo fmt --check 74 | @echo "✅ Code formatting check completed" 75 | 76 | # Run clippy linter 77 | .PHONY: clippy 78 | clippy: 79 | @echo "🔍 Running clippy linter..." 80 | cargo clippy -- -D warnings 81 | @echo "✅ Clippy check completed" 82 | 83 | # Full code quality check 84 | .PHONY: check 85 | check: fmt clippy test 86 | @echo "✅ All code quality checks passed" 87 | 88 | # Install development dependencies 89 | .PHONY: install-dev 90 | install-dev: 91 | @echo "📦 Installing development dependencies..." 92 | cargo install cargo-watch 93 | @echo "✅ Development dependencies installed" 94 | 95 | # Show help 96 | .PHONY: help 97 | help: 98 | @echo "Nylon Proxy Server - Available targets:" 99 | @echo "" 100 | @echo " dev - Start development server with hot reload" 101 | @echo " dev-debug - Start development server with debug logging" 102 | @echo " build - Build release version" 103 | @echo " build-examples - Build Go plugin examples" 104 | @echo " generate - Generate FlatBuffers code" 105 | @echo " test - Run tests" 106 | @echo " fmt - Check code formatting" 107 | @echo " clippy - Run clippy linter" 108 | @echo " check - Run all code quality checks" 109 | @echo " clean - Clean build artifacts" 110 | @echo " install-dev - Install development dependencies" 111 | @echo " help - Show this help message" 112 | -------------------------------------------------------------------------------- /crates/nylon/src/context.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use nylon_error::NylonError; 3 | use nylon_types::context::NylonContext; 4 | use pingora::proxy::Session; 5 | use std::sync::atomic::Ordering; 6 | 7 | #[async_trait] 8 | pub trait NylonContextExt { 9 | async fn parse_request(&self, session: &mut Session) -> Result<(), NylonError>; 10 | } 11 | 12 | #[async_trait] 13 | impl NylonContextExt for NylonContext { 14 | async fn parse_request(&self, session: &mut Session) -> Result<(), NylonError> { 15 | { 16 | let mut client_ip = self 17 | .client_ip 18 | .write() 19 | .map_err(|_| NylonError::InternalServerError("lock poisoned".into()))?; 20 | *client_ip = match session.client_addr() { 21 | Some(ip) => match ip.as_inet() { 22 | Some(ip) => ip.ip().to_string(), 23 | None => { 24 | return Err(NylonError::HttpException( 25 | 400, 26 | "BAD_REQUEST", 27 | "Unable to get client IP", 28 | )); 29 | } 30 | }, 31 | None => { 32 | return Err(NylonError::HttpException( 33 | 400, 34 | "BAD_REQUEST", 35 | "Unable to get client IP", 36 | )); 37 | } 38 | }; 39 | } 40 | let is_tls = match session.digest() { 41 | Some(d) => d.ssl_digest.is_some(), 42 | None => false, 43 | }; 44 | self.tls.store(is_tls, Ordering::Relaxed); 45 | // reset per-request caches 46 | { 47 | if let Ok(mut q) = self.cached_query.write() { 48 | *q = None; 49 | } 50 | if let Ok(mut c) = self.cached_cookies.write() { 51 | *c = None; 52 | } 53 | } 54 | match session.as_http2() { 55 | Some(session) => { 56 | let host = session.req_header().uri.host().unwrap_or(""); 57 | let mut h = self 58 | .host 59 | .write() 60 | .map_err(|_| NylonError::InternalServerError("lock poisoned".into()))?; 61 | *h = host.to_string(); 62 | let mut p = self 63 | .port 64 | .write() 65 | .map_err(|_| NylonError::InternalServerError("lock poisoned".into()))?; 66 | *p = "".to_string(); 67 | } 68 | None => { 69 | let host = match session.req_header().headers.get("Host") { 70 | Some(h) => { 71 | // h.to_str().unwrap_or("").split(':').next().unwrap_or("") 72 | let host = h.to_str().unwrap_or("").split(':').next().unwrap_or(""); 73 | let port = h.to_str().unwrap_or("").split(':').nth(1).unwrap_or(""); 74 | let mut p = self 75 | .port 76 | .write() 77 | .map_err(|_| NylonError::InternalServerError("lock poisoned".into()))?; 78 | *p = port.to_string(); 79 | host 80 | } 81 | None => "", 82 | }; 83 | let mut h = self 84 | .host 85 | .write() 86 | .map_err(|_| NylonError::InternalServerError("lock poisoned".into()))?; 87 | *h = host.to_string(); 88 | } 89 | } 90 | Ok(()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /docs/introduction/what-is-nylon.md: -------------------------------------------------------------------------------- 1 | # What is Nylon? 2 | 3 | Nylon is a high-performance HTTP/HTTPS reverse proxy built with Rust, powered by Cloudflare's Pingora framework. It's designed to be fast, reliable, and highly extensible through its plugin system. 4 | 5 | ## Why Nylon? 6 | 7 | ### ⚡️ Performance First 8 | 9 | Built on top of Pingora, the same technology that powers Cloudflare's edge network, Nylon delivers: 10 | - Low latency request handling 11 | - Efficient memory usage 12 | - High throughput 13 | - Connection pooling and reuse 14 | 15 | ### 🔌 Plugin Ecosystem 16 | 17 | Extend Nylon's functionality with plugins: 18 | - Request/Response filtering 19 | - Authentication and authorization 20 | - Custom logging and metrics 21 | - WebSocket message handling 22 | - Go SDK ready, more languages coming soon 23 | 24 | ### 🎯 Enterprise Features 25 | 26 | - **Multiple Load Balancing Strategies**: Round Robin, Weighted, Consistent Hashing, Random 27 | - **TLS/HTTPS Support**: Automatic certificate management with ACME (Let's Encrypt) 28 | - **Advanced Routing**: Path-based and host-based routing with parameter extraction 29 | - **Dynamic Configuration**: Hot-reload configuration without downtime 30 | - **Observability**: Comprehensive logging with request/response metrics 31 | 32 | ## Use Cases 33 | 34 | ### API Gateway 35 | Use Nylon as a centralized entry point for your microservices: 36 | - Route requests to appropriate services 37 | - Handle authentication and authorization 38 | - Rate limiting and throttling 39 | - Request/response transformation 40 | 41 | ### Load Balancer 42 | Distribute traffic across multiple backend servers: 43 | - Health checks 44 | - Connection pooling 45 | - Automatic failover 46 | - Session persistence 47 | 48 | ### WebSocket Proxy 49 | Proxy WebSocket connections with: 50 | - Message filtering and transformation 51 | - Room-based broadcasting 52 | - Connection management 53 | 54 | ## Architecture 55 | 56 | ``` 57 | ┌─────────────┐ 58 | │ Client │ 59 | └──────┬──────┘ 60 | │ 61 | │ HTTP/HTTPS/WebSocket 62 | │ 63 | ┌──────▼──────────────────────┐ 64 | │ Nylon Proxy │ 65 | │ ┌────────────────────────┐ │ 66 | │ │ Plugin System │ │ 67 | │ │ ┌──────────────────┐ │ │ 68 | │ │ │ Request Filter │ │ │ 69 | │ │ │ Response Filter │ │ │ 70 | │ │ │ Body Filter │ │ │ 71 | │ │ │ Logging │ │ │ 72 | │ │ └──────────────────┘ │ │ 73 | │ └────────────────────────┘ │ 74 | │ ┌────────────────────────┐ │ 75 | │ │ Routing Engine │ │ 76 | │ └────────────────────────┘ │ 77 | │ ┌────────────────────────┐ │ 78 | │ │ Load Balancer │ │ 79 | │ └────────────────────────┘ │ 80 | └──────┬──────────────────────┘ 81 | │ 82 | │ Multiple strategies 83 | │ 84 | ┌──────▼──────┐ ┌─────────────┐ ┌─────────────┐ 85 | │ Backend 1 │ │ Backend 2 │ │ Backend 3 │ 86 | └─────────────┘ └─────────────┘ └─────────────┘ 87 | ``` 88 | 89 | ## What Makes Nylon Different? 90 | 91 | - **🦀 Built with Rust on Pingora** - Leverages Cloudflare's battle-tested framework for unmatched performance and reliability 92 | - **🔌 Flexible Plugin System** - FFI-based architecture supporting multiple languages (Go SDK ready, more coming) 93 | - **⚡️ True Zero-Downtime** - Hot reload configuration and code without dropping a single connection 94 | - **🔒 Security First** - Automatic TLS with ACME, built-in security headers, and safe plugin isolation 95 | - **📊 Observable by Default** - Comprehensive logging, metrics, and health checks out of the box 96 | - **🎯 Developer Friendly** - Clean YAML config, intuitive APIs, and extensive documentation 97 | 98 | ## Next Steps 99 | 100 |
101 |

Ready to get started?

102 |

Check out the Quick Start guide to begin using Nylon.

103 |
104 | 105 | -------------------------------------------------------------------------------- /crates/nylon-error/src/lib.rs: -------------------------------------------------------------------------------- 1 | use serde_json::{Value, json}; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug, Clone)] 5 | pub enum NylonError { 6 | #[error("Failed to parse configuration: {0}")] 7 | ConfigError(String), 8 | 9 | #[error("Could not start the Pingora server: {0}")] 10 | PingoraError(String), 11 | 12 | #[error("Runtime error occurred: {0}")] 13 | RuntimeError(String), 14 | 15 | #[error("{{ \"status\": \"{0}\", \"error\": \"{1}\", \"message\": \"{2}\" }}")] 16 | HttpException(u16, &'static str, &'static str), 17 | 18 | #[error("Requested service is unavailable: {0}")] 19 | ServiceNotFound(String), 20 | 21 | #[error("No route matched the request: {0}")] 22 | RouteNotFound(String), 23 | 24 | #[error("Unable to generate ACME key pair: {0}")] 25 | AcmeKeyPairError(String), 26 | 27 | #[error("ACME HTTP request encountered an error: {0}")] 28 | AcmeHttpClientError(String), 29 | 30 | #[error("ACME JWS signing failed: {0}")] 31 | AcmeJWSError(String), 32 | 33 | #[error("ACME client encountered an error: {0}")] 34 | AcmeClientError(String), 35 | 36 | #[error("An unexpected internal server error occurred: {0}")] 37 | InternalServerError(String), 38 | 39 | #[error( 40 | "[BUG] This should never happen. Please report it at https://github.com/AssetsArt/nylon: {0}" 41 | )] 42 | ShouldNeverHappen(String), 43 | } 44 | 45 | impl NylonError { 46 | pub fn http_status(&self) -> u16 { 47 | match self { 48 | NylonError::HttpException(status, _, _) => *status, 49 | _ => 500, 50 | } 51 | } 52 | 53 | pub fn error_code(&self) -> String { 54 | match self { 55 | NylonError::HttpException(_, error, _) => error.to_string(), 56 | NylonError::ConfigError(_) => "CONFIG_ERROR".to_string(), 57 | NylonError::PingoraError(_) => "PINGORA_ERROR".to_string(), 58 | NylonError::RuntimeError(_) => "RUNTIME_ERROR".to_string(), 59 | NylonError::ServiceNotFound(_) => "SERVICE_NOT_FOUND".to_string(), 60 | NylonError::RouteNotFound(_) => "ROUTE_NOT_FOUND".to_string(), 61 | NylonError::AcmeKeyPairError(_) => "ACME_KEY_PAIR_ERROR".to_string(), 62 | NylonError::AcmeHttpClientError(_) => "ACME_HTTP_CLIENT_ERROR".to_string(), 63 | NylonError::AcmeJWSError(_) => "ACME_JWS_ERROR".to_string(), 64 | NylonError::AcmeClientError(_) => "ACME_CLIENT_ERROR".to_string(), 65 | NylonError::InternalServerError(_) => "INTERNAL_SERVER_ERROR".to_string(), 66 | NylonError::ShouldNeverHappen(_) => "SHOULD_NEVER_HAPPEN".to_string(), 67 | } 68 | } 69 | 70 | pub fn message(&self) -> String { 71 | match self { 72 | NylonError::HttpException(_, _, message) => message.to_string(), 73 | NylonError::ConfigError(message) => message.to_string(), 74 | NylonError::PingoraError(message) => message.to_string(), 75 | NylonError::RuntimeError(message) => message.to_string(), 76 | NylonError::ServiceNotFound(message) => message.to_string(), 77 | NylonError::RouteNotFound(message) => message.to_string(), 78 | NylonError::AcmeKeyPairError(message) => message.to_string(), 79 | NylonError::AcmeHttpClientError(message) => message.to_string(), 80 | NylonError::AcmeJWSError(message) => message.to_string(), 81 | NylonError::AcmeClientError(message) => message.to_string(), 82 | NylonError::InternalServerError(message) => message.to_string(), 83 | NylonError::ShouldNeverHappen(message) => format!( 84 | "[BUG] This should never happen. Please report it at https://github.com/AssetsArt/nylon: {}", 85 | message 86 | ), 87 | } 88 | } 89 | 90 | pub fn exception_json(&self) -> Value { 91 | json!({ 92 | "status": self.http_status(), 93 | "error": self.error_code(), 94 | "message": self.message(), 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /docs/examples/basic-proxy.md: -------------------------------------------------------------------------------- 1 | # Basic Proxy 2 | 3 | A simple reverse proxy example forwarding HTTP requests to a backend. 4 | 5 | ## Configuration 6 | 7 | ### Runtime Config (`config.yaml`) 8 | 9 | ```yaml 10 | http: 11 | - "0.0.0.0:8080" 12 | 13 | config_dir: "./config" 14 | 15 | pingora: 16 | daemon: false 17 | threads: 4 18 | work_stealing: true 19 | ``` 20 | 21 | ### Proxy Config (`config/proxy.yaml`) 22 | 23 | ```yaml 24 | services: 25 | - name: backend 26 | service_type: http 27 | algorithm: round_robin 28 | endpoints: 29 | - ip: 127.0.0.1 30 | port: 3000 31 | 32 | routes: 33 | - route: 34 | type: host 35 | value: localhost 36 | name: default 37 | paths: 38 | - path: 39 | - / 40 | - /{*path} 41 | service: 42 | name: backend 43 | ``` 44 | 45 | ## Running 46 | 47 | ```bash 48 | nylon run -c config.yaml 49 | ``` 50 | 51 | ## Testing 52 | 53 | ```bash 54 | # Make a request 55 | curl http://localhost:8080 56 | 57 | # With headers 58 | curl -H "X-Custom-Header: value" http://localhost:8080/api 59 | 60 | # POST request 61 | curl -X POST -d '{"key":"value"}' \ 62 | -H "Content-Type: application/json" \ 63 | http://localhost:8080/api 64 | ``` 65 | 66 | ## Multiple Backends 67 | 68 | Add more endpoints for load balancing: 69 | 70 | ```yaml 71 | services: 72 | - name: backend 73 | service_type: http 74 | algorithm: round_robin 75 | endpoints: 76 | - ip: 127.0.0.1 77 | port: 3000 78 | - ip: 127.0.0.1 79 | port: 3001 80 | - ip: 127.0.0.1 81 | port: 3002 82 | ``` 83 | 84 | ## Health Checks 85 | 86 | Enable health checking: 87 | 88 | ```yaml 89 | services: 90 | - name: backend 91 | service_type: http 92 | algorithm: round_robin 93 | endpoints: 94 | - ip: 127.0.0.1 95 | port: 3000 96 | - ip: 127.0.0.1 97 | port: 3001 98 | health_check: 99 | enabled: true 100 | path: /health 101 | interval: 5s 102 | timeout: 2s 103 | healthy_threshold: 2 104 | unhealthy_threshold: 3 105 | ``` 106 | 107 | ## Path-Based Routing 108 | 109 | Route different paths to different services: 110 | 111 | ```yaml 112 | services: 113 | - name: api-service 114 | service_type: http 115 | algorithm: round_robin 116 | endpoints: 117 | - ip: 127.0.0.1 118 | port: 3000 119 | 120 | - name: admin-service 121 | service_type: http 122 | algorithm: round_robin 123 | endpoints: 124 | - ip: 127.0.0.1 125 | port: 4000 126 | 127 | routes: 128 | - route: 129 | type: host 130 | value: localhost 131 | name: default 132 | paths: 133 | - path: /api/{*path} 134 | service: 135 | name: api-service 136 | 137 | - path: /admin/{*path} 138 | service: 139 | name: admin-service 140 | ``` 141 | 142 | ## Host-Based Routing 143 | 144 | Route by hostname: 145 | 146 | ```yaml 147 | services: 148 | - name: app1-service 149 | service_type: http 150 | algorithm: round_robin 151 | endpoints: 152 | - ip: 127.0.0.1 153 | port: 3000 154 | 155 | - name: app2-service 156 | service_type: http 157 | algorithm: round_robin 158 | endpoints: 159 | - ip: 127.0.0.1 160 | port: 4000 161 | 162 | routes: 163 | - route: 164 | type: host 165 | value: app1.example.com 166 | name: app1 167 | paths: 168 | - path: 169 | - / 170 | - /{*path} 171 | service: 172 | name: app1-service 173 | 174 | - route: 175 | type: host 176 | value: app2.example.com 177 | name: app2 178 | paths: 179 | - path: 180 | - / 181 | - /{*path} 182 | service: 183 | name: app2-service 184 | ``` 185 | 186 | ## Next Steps 187 | 188 | - [Authentication Example](/examples/authentication) 189 | - [Static Files](/core/configuration#static-service) 190 | - [Load Balancing](/core/configuration#services) 191 | -------------------------------------------------------------------------------- /docs/plugins/go-sdk.md: -------------------------------------------------------------------------------- 1 | # Go SDK 2 | 3 | Complete Go SDK API reference. Pair this document with the [plugin overview](/plugins/overview) for end-to-end examples. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | go get github.com/AssetsArt/nylon/sdk/go/sdk 9 | ``` 10 | 11 | ## Basic Usage 12 | 13 | ```go 14 | package main 15 | 16 | import "C" 17 | import sdk "github.com/AssetsArt/nylon/sdk/go/sdk" 18 | 19 | func main() {} 20 | 21 | func init() { 22 | plugin := sdk.NewNylonPlugin() 23 | 24 | plugin.AddPhaseHandler("handler", func(phase *sdk.PhaseHandler) { 25 | phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) { 26 | // Gate, mutate, or short-circuit the request. 27 | ctx.Next() 28 | }) 29 | }) 30 | } 31 | ``` 32 | 33 | ## API Reference 34 | 35 | ### Request helpers 36 | 37 | | Method | Description | 38 | |--------|-------------| 39 | | `req.Method()` | HTTP method (`GET`, `POST`, …). | 40 | | `req.Path()` | Request path. | 41 | | `req.URL()` | Full URL (scheme + host + path + query). | 42 | | `req.Query()` | Raw query string. | 43 | | `req.Params()` | Route parameters (`map[string]string`). | 44 | | `req.Header(name)` | Single header value. | 45 | | `req.Headers()` | Iterator with `.Get`/`.GetAll()` helpers. | 46 | | `req.RawBody()` | Request body (lazy-loaded). | 47 | | `req.Host()` | Host header. | 48 | | `req.ClientIP()` | Client IP address. | 49 | | `req.Timestamp()` | Request timestamp (milliseconds). | 50 | | `req.Bytes()` | Request body size. | 51 | 52 | ### Response helpers 53 | 54 | | Method | Description | 55 | |--------|-------------| 56 | | `res.SetStatus(code)` | Set status code. | 57 | | `res.Status()` | Retrieve status. | 58 | | `res.SetHeader(name, value)` | Set/overwrite header. | 59 | | `res.RemoveHeader(name)` | Remove header. | 60 | | `res.Headers()` | Map of response headers. | 61 | | `res.BodyRaw([]byte)` | Replace body with bytes. | 62 | | `res.BodyText(string)` | Convenience for UTF-8 text. | 63 | | `res.BodyJSON(any)` | Marshal and send JSON. | 64 | | `res.ReadBody()` | Read upstream body (response body filter / logging). | 65 | | `res.Redirect(url, code...)` | Issue redirect (default 302). | 66 | | `res.Bytes()` | Response size. | 67 | | `res.Duration()` | Elapsed time in ms. | 68 | | `res.Error()` | Captured upstream errors. | 69 | | `res.Stream()` | Start streaming response. | 70 | 71 | > `ctx.GetPayload()` is available on every phase context and returns the static payload configured in your YAML middleware entry. 72 | 73 | ## WebSocket helper APIs 74 | 75 | Upgrade an HTTP connection to WebSocket and register callbacks: 76 | 77 | ```go 78 | callbacks := sdk.WebSocketCallbacks{ 79 | OnOpen: func(ws *sdk.WebSocketConn) { 80 | ws.JoinRoom("lobby") 81 | ws.SendText("Welcome!") 82 | }, 83 | OnMessageText: func(ws *sdk.WebSocketConn, msg string) { 84 | ws.BroadcastText("lobby", msg) 85 | }, 86 | OnClose: func(ws *sdk.WebSocketConn) { 87 | ws.LeaveRoom("lobby") 88 | }, 89 | } 90 | 91 | if err := ctx.WebSocketUpgrade(callbacks); err != nil { 92 | res := ctx.Response() 93 | res.RemoveHeader("Content-Length") 94 | res.SetHeader("Transfer-Encoding", "chunked") 95 | res.SetStatus(400) 96 | res.BodyText("Upgrade failed") 97 | ctx.End() 98 | return 99 | } 100 | 101 | ctx.Next() 102 | ``` 103 | 104 | Connection methods: 105 | 106 | | Method | Description | 107 | |--------|-------------| 108 | | `SendText(message)` | Send text frame. | 109 | | `SendBinary(data)` | Send binary frame. | 110 | | `Close()` | Close connection. | 111 | | `JoinRoom(room)` / `LeaveRoom(room)` | Convenience helpers for broadcast rooms. | 112 | | `BroadcastText(room, message)` | Broadcast text to room. | 113 | | `BroadcastBinary(room, data)` | Broadcast binary payload to room. | 114 | 115 | ## Streaming responses 116 | 117 | ```go 118 | stream, err := ctx.Response().Stream() 119 | if err != nil { 120 | log.Printf("stream error: %v", err) 121 | ctx.End() 122 | return 123 | } 124 | 125 | defer stream.End() 126 | stream.Write([]byte("chunk 1")) 127 | stream.Write([]byte("chunk 2")) 128 | ``` 129 | 130 | ## Error handling patterns 131 | 132 | ```go 133 | if err := ctx.WebSocketUpgrade(callbacks); err != nil { 134 | res := ctx.Response() 135 | res.RemoveHeader("Content-Length") 136 | res.SetHeader("Transfer-Encoding", "chunked") 137 | res.SetStatus(400) 138 | res.BodyText("Error") 139 | ctx.End() 140 | return 141 | } 142 | 143 | ctx.Next() 144 | ``` 145 | 146 | ## Best practices 147 | 148 | 1. **Always call `ctx.Next()`** unless you explicitly terminate the request with `ctx.End()`. 149 | 2. **Handle errors** – return proper status codes and chunked responses to avoid hangs. 150 | 3. **Avoid long blocking work** inside phase handlers; offload to goroutines if necessary. 151 | 4. **Use middleware payloads** for configuration; they are exposed through `ctx.GetPayload()` in every phase. 152 | -------------------------------------------------------------------------------- /crates/nylon-plugin/src/loaders.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::ffi_symbols; 2 | use dashmap::DashMap; 3 | use libloading::{Library, Symbol}; 4 | use nylon_types::plugins::{ 5 | FfiCloseSessionFn, FfiEventStreamFn, FfiInitializeFn, FfiPlugin, FfiPluginFreeFn, 6 | FfiRegisterSessionFn, FfiShutdownFn, PluginItem, 7 | }; 8 | use std::sync::Arc; 9 | 10 | pub fn load(plugin: &PluginItem) { 11 | let file = plugin.file.clone(); 12 | let lib_store = 13 | match nylon_store::get::>>(nylon_store::KEY_LIBRARY_FILE) { 14 | Some(lib) => lib, 15 | None => { 16 | let new_lib = DashMap::new(); 17 | nylon_store::insert(nylon_store::KEY_LIBRARY_FILE, new_lib.clone()); 18 | new_lib 19 | } 20 | }; 21 | 22 | let lib = match lib_store.get(&file) { 23 | Some(lib) => lib, 24 | None => { 25 | let lib = unsafe { 26 | match Library::new(&file) { 27 | Ok(lib) => lib, 28 | Err(e) => { 29 | eprintln!("Failed to load shared library: {}", e); 30 | return; 31 | } 32 | } 33 | }; 34 | lib_store.insert(file.clone(), Arc::new(lib)); 35 | match lib_store.get(&file) { 36 | Some(lib) => lib, 37 | None => { 38 | eprintln!("Failed to get loaded library"); 39 | return; 40 | } 41 | } 42 | } 43 | }; 44 | let plugin_free = unsafe { 45 | let symbol: Symbol = lib 46 | .get(ffi_symbols::PLUGIN_FREE.as_bytes()) 47 | .unwrap_or_else(|_| { 48 | panic!("Failed to load symbol: {}", ffi_symbols::PLUGIN_FREE); 49 | }); 50 | std::mem::transmute::, Symbol<'static, FfiPluginFreeFn>>(symbol) 51 | }; 52 | let register_session = unsafe { 53 | let symbol: Symbol = lib 54 | .get(ffi_symbols::REGISTER_SESSION.as_bytes()) 55 | .unwrap_or_else(|_| { 56 | panic!("Failed to load symbol: {}", ffi_symbols::REGISTER_SESSION); 57 | }); 58 | std::mem::transmute::, Symbol<'static, FfiRegisterSessionFn>>( 59 | symbol, 60 | ) 61 | }; 62 | let event_stream = unsafe { 63 | let symbol: Symbol = lib 64 | .get(ffi_symbols::EVENT_STREAM.as_bytes()) 65 | .unwrap_or_else(|_| { 66 | panic!("Failed to load symbol: {}", ffi_symbols::EVENT_STREAM); 67 | }); 68 | std::mem::transmute::, Symbol<'static, FfiEventStreamFn>>(symbol) 69 | }; 70 | let close_session = unsafe { 71 | let symbol: Symbol = lib 72 | .get(ffi_symbols::CLOSE_SESSION.as_bytes()) 73 | .unwrap_or_else(|_| { 74 | panic!("Failed to load symbol: {}", ffi_symbols::CLOSE_SESSION); 75 | }); 76 | std::mem::transmute::, Symbol<'static, FfiCloseSessionFn>>(symbol) 77 | }; 78 | let shutdown = unsafe { 79 | let symbol: Symbol = lib 80 | .get(ffi_symbols::SHUTDOWN.as_bytes()) 81 | .unwrap_or_else(|_| { 82 | panic!("Failed to load symbol: {}", ffi_symbols::SHUTDOWN); 83 | }); 84 | std::mem::transmute::, Symbol<'static, FfiShutdownFn>>(symbol) 85 | }; 86 | 87 | let ffi_item = FfiPlugin { 88 | _lib: lib.clone(), 89 | plugin_free, 90 | register_session, 91 | event_stream, 92 | close_session, 93 | shutdown, 94 | }; 95 | let plugins = 96 | match nylon_store::get::>>(nylon_store::KEY_PLUGINS) { 97 | Some(plugins) => plugins, 98 | None => { 99 | let new_plugins = DashMap::new(); 100 | nylon_store::insert(nylon_store::KEY_PLUGINS, new_plugins.clone()); 101 | new_plugins 102 | } 103 | }; 104 | plugins.insert(plugin.name.clone(), Arc::new(ffi_item)); 105 | nylon_store::insert(nylon_store::KEY_PLUGINS, plugins); 106 | 107 | // initialize 108 | let initialize = unsafe { 109 | let symbol: Symbol = lib 110 | .get(ffi_symbols::INITIALIZE.as_bytes()) 111 | .unwrap_or_else(|_| { 112 | panic!("Failed to load symbol: {}", ffi_symbols::INITIALIZE); 113 | }); 114 | std::mem::transmute::, Symbol<'static, FfiInitializeFn>>(symbol) 115 | }; 116 | let config = match &plugin.config { 117 | Some(config) => serde_json::to_string(&config).unwrap_or_default(), 118 | None => "".to_string(), 119 | }; 120 | let config_ptr = config.as_ptr(); 121 | let config_len = config.len() as u32; 122 | unsafe { 123 | initialize(config_ptr, config_len); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /crates/nylon-types/src/context.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::type_complexity)] 2 | 3 | use crate::{plugins::SessionStream, route::MiddlewareItem, services::ServiceItem, template::Expr}; 4 | use pingora::lb::Backend; 5 | use std::{ 6 | collections::HashMap, 7 | sync::{ 8 | RwLock, 9 | atomic::{AtomicBool, AtomicU16, AtomicU64, Ordering}, 10 | }, 11 | }; 12 | 13 | #[derive(Debug, Clone)] 14 | pub struct Route { 15 | pub service: ServiceItem, 16 | pub rewrite: Option, 17 | pub route_middleware: Option>>)>>, 18 | pub path_middleware: Option>>)>>, 19 | pub payload_ast: Option>>, 20 | } 21 | 22 | #[derive(Debug)] 23 | pub struct NylonContext { 24 | pub backend: RwLock, 25 | pub client_ip: RwLock, 26 | pub route: RwLock>, 27 | pub params: RwLock>>, 28 | pub host: RwLock, 29 | pub port: RwLock, 30 | pub tls: AtomicBool, 31 | pub session_ids: RwLock>, 32 | pub session_stream: RwLock>, 33 | pub add_response_header: RwLock>, 34 | pub remove_response_header: RwLock>, 35 | pub set_response_status: AtomicU16, 36 | pub set_response_body: RwLock>, 37 | pub read_body: AtomicBool, 38 | pub request_body: RwLock>, 39 | // Caches per request to avoid repeated parsing 40 | pub cached_query: RwLock>>, 41 | pub cached_cookies: RwLock>>, 42 | // Logging information 43 | pub request_timestamp: AtomicU64, 44 | pub error_message: RwLock>, 45 | } 46 | 47 | impl Default for NylonContext { 48 | fn default() -> Self { 49 | Self { 50 | // Backend and routing 51 | backend: RwLock::new( 52 | Backend::new("127.0.0.1:80").expect("Unable to create default backend"), 53 | ), 54 | client_ip: RwLock::new("127.0.0.1".to_string()), 55 | route: RwLock::new(None), 56 | params: RwLock::new(None), 57 | host: RwLock::new("".to_string()), 58 | port: RwLock::new("".to_string()), 59 | tls: AtomicBool::new(false), 60 | session_ids: RwLock::new(HashMap::new()), 61 | session_stream: RwLock::new(HashMap::new()), 62 | 63 | // Response modifications 64 | add_response_header: RwLock::new(HashMap::new()), 65 | remove_response_header: RwLock::new(Vec::new()), 66 | set_response_status: AtomicU16::new(200), 67 | set_response_body: RwLock::new(Vec::new()), 68 | 69 | // Request modifications 70 | read_body: AtomicBool::new(false), 71 | request_body: RwLock::new(Vec::new()), 72 | 73 | // Request caches 74 | cached_query: RwLock::new(None), 75 | cached_cookies: RwLock::new(None), 76 | 77 | // Logging information 78 | request_timestamp: AtomicU64::new(0), 79 | error_message: RwLock::new(None), 80 | } 81 | } 82 | } 83 | 84 | impl Clone for NylonContext { 85 | fn clone(&self) -> Self { 86 | Self { 87 | backend: RwLock::new(self.backend.read().expect("lock").clone()), 88 | client_ip: RwLock::new(self.client_ip.read().expect("lock").clone()), 89 | route: RwLock::new(self.route.read().expect("lock").clone()), 90 | params: RwLock::new(self.params.read().expect("lock").clone()), 91 | host: RwLock::new(self.host.read().expect("lock").clone()), 92 | port: RwLock::new(self.port.read().expect("lock").clone()), 93 | tls: AtomicBool::new(self.tls.load(Ordering::Relaxed)), 94 | session_ids: RwLock::new(self.session_ids.read().expect("lock").clone()), 95 | session_stream: RwLock::new(self.session_stream.read().expect("lock").clone()), 96 | add_response_header: RwLock::new( 97 | self.add_response_header.read().expect("lock").clone(), 98 | ), 99 | remove_response_header: RwLock::new( 100 | self.remove_response_header.read().expect("lock").clone(), 101 | ), 102 | set_response_status: AtomicU16::new(self.set_response_status.load(Ordering::Relaxed)), 103 | set_response_body: RwLock::new(self.set_response_body.read().expect("lock").clone()), 104 | read_body: AtomicBool::new(self.read_body.load(Ordering::Relaxed)), 105 | request_body: RwLock::new(self.request_body.read().expect("lock").clone()), 106 | cached_query: RwLock::new(self.cached_query.read().expect("lock").clone()), 107 | cached_cookies: RwLock::new(self.cached_cookies.read().expect("lock").clone()), 108 | request_timestamp: AtomicU64::new(self.request_timestamp.load(Ordering::Relaxed)), 109 | error_message: RwLock::new(self.error_message.read().expect("lock").clone()), 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /sdk/go/sdk/constants.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | type NylonMethods string 4 | 5 | const ( 6 | NylonMethodNext NylonMethods = "next" 7 | NylonMethodEnd NylonMethods = "end" 8 | NylonMethodGetPayload NylonMethods = "get_payload" 9 | ) 10 | 11 | const ( 12 | NylonMethodSetResponseHeader NylonMethods = "set_response_header" 13 | NylonMethodRemoveResponseHeader NylonMethods = "remove_response_header" 14 | NylonMethodSetResponseStatus NylonMethods = "set_response_status" 15 | NylonMethodSetResponseFullBody NylonMethods = "set_response_full_body" 16 | NylonMethodSetResponseStreamData NylonMethods = "set_response_stream_data" 17 | NylonMethodSetResponseStreamEnd NylonMethods = "set_response_stream_end" 18 | NylonMethodSetResponseStreamHeader NylonMethods = "set_response_stream_header" 19 | NylonMethodReadResponseFullBody NylonMethods = "read_response_full_body" 20 | ) 21 | 22 | const ( 23 | NylonMethodReadRequestFullBody NylonMethods = "read_request_full_body" 24 | NylonMethodReadRequestHeader NylonMethods = "read_request_header" 25 | NylonMethodReadRequestHeaders NylonMethods = "read_request_headers" 26 | NylonMethodReadRequestURL NylonMethods = "read_request_url" 27 | NylonMethodReadRequestPath NylonMethods = "read_request_path" 28 | NylonMethodReadRequestQuery NylonMethods = "read_request_query" 29 | NylonMethodReadRequestParams NylonMethods = "read_request_params" 30 | NylonMethodReadRequestHost NylonMethods = "read_request_host" 31 | NylonMethodReadRequestClientIP NylonMethods = "read_request_client_ip" 32 | NylonMethodReadRequestMethod NylonMethods = "read_request_method" 33 | NylonMethodReadRequestBytes NylonMethods = "read_request_bytes" 34 | NylonMethodReadRequestTimestamp NylonMethods = "read_request_timestamp" 35 | NylonMethodReadResponseStatus NylonMethods = "read_response_status" 36 | NylonMethodReadResponseBytes NylonMethods = "read_response_bytes" 37 | NylonMethodReadResponseHeaders NylonMethods = "read_response_headers" 38 | NylonMethodReadResponseDuration NylonMethods = "read_response_duration" 39 | NylonMethodReadResponseError NylonMethods = "read_response_error" 40 | ) 41 | 42 | // WebSocket methods 43 | const ( 44 | // Plugin -> Rust 45 | NylonMethodWebSocketUpgrade NylonMethods = "websocket_upgrade" 46 | NylonMethodWebSocketSendText NylonMethods = "websocket_send_text" 47 | NylonMethodWebSocketSendBinary NylonMethods = "websocket_send_binary" 48 | NylonMethodWebSocketClose NylonMethods = "websocket_close" 49 | 50 | // WebSocket room methods (Plugin -> Rust) 51 | NylonMethodWebSocketJoinRoom NylonMethods = "websocket_join_room" 52 | NylonMethodWebSocketLeaveRoom NylonMethods = "websocket_leave_room" 53 | NylonMethodWebSocketBroadcastRoomText NylonMethods = "websocket_broadcast_room_text" 54 | NylonMethodWebSocketBroadcastRoomBinary NylonMethods = "websocket_broadcast_room_binary" 55 | 56 | // Rust -> Plugin 57 | NylonMethodWebSocketOnOpen NylonMethods = "websocket_on_open" 58 | NylonMethodWebSocketOnMessageText NylonMethods = "websocket_on_message_text" 59 | NylonMethodWebSocketOnMessageBinary NylonMethods = "websocket_on_message_binary" 60 | NylonMethodWebSocketOnClose NylonMethods = "websocket_on_close" 61 | NylonMethodWebSocketOnError NylonMethods = "websocket_on_error" 62 | ) 63 | 64 | var MethodIDMapping = map[NylonMethods]uint32{ 65 | NylonMethodNext: 1, 66 | NylonMethodEnd: 2, 67 | NylonMethodGetPayload: 3, 68 | 69 | // Response methods 70 | NylonMethodSetResponseHeader: 100, 71 | NylonMethodRemoveResponseHeader: 101, 72 | NylonMethodSetResponseStatus: 102, 73 | NylonMethodSetResponseFullBody: 103, 74 | NylonMethodSetResponseStreamData: 104, 75 | NylonMethodSetResponseStreamEnd: 105, 76 | NylonMethodSetResponseStreamHeader: 106, 77 | NylonMethodReadResponseFullBody: 107, 78 | 79 | // Request methods 80 | NylonMethodReadRequestFullBody: 200, 81 | NylonMethodReadRequestHeader: 201, 82 | NylonMethodReadRequestHeaders: 202, 83 | NylonMethodReadRequestURL: 203, 84 | NylonMethodReadRequestPath: 204, 85 | NylonMethodReadRequestQuery: 205, 86 | NylonMethodReadRequestParams: 206, 87 | NylonMethodReadRequestHost: 207, 88 | NylonMethodReadRequestClientIP: 208, 89 | NylonMethodReadRequestMethod: 209, 90 | NylonMethodReadRequestBytes: 210, 91 | NylonMethodReadRequestTimestamp: 211, 92 | NylonMethodReadResponseStatus: 108, 93 | NylonMethodReadResponseBytes: 109, 94 | NylonMethodReadResponseHeaders: 110, 95 | NylonMethodReadResponseDuration: 111, 96 | NylonMethodReadResponseError: 112, 97 | 98 | // WebSocket methods 99 | NylonMethodWebSocketUpgrade: 300, 100 | NylonMethodWebSocketSendText: 301, 101 | NylonMethodWebSocketSendBinary: 302, 102 | NylonMethodWebSocketClose: 303, 103 | NylonMethodWebSocketJoinRoom: 310, 104 | NylonMethodWebSocketLeaveRoom: 311, 105 | NylonMethodWebSocketBroadcastRoomText: 312, 106 | NylonMethodWebSocketBroadcastRoomBinary: 313, 107 | NylonMethodWebSocketOnOpen: 350, 108 | NylonMethodWebSocketOnMessageText: 351, 109 | NylonMethodWebSocketOnMessageBinary: 352, 110 | NylonMethodWebSocketOnClose: 353, 111 | NylonMethodWebSocketOnError: 354, 112 | } 113 | 114 | const ( 115 | StatusOK = 200 116 | StatusFound = 302 117 | StatusBadRequest = 400 118 | StatusUnauthorized = 401 119 | StatusForbidden = 403 120 | StatusNotFound = 404 121 | StatusTooManyRequests = 429 122 | StatusInternalServerError = 500 123 | ) 124 | 125 | const ( 126 | ContentTypeJSON = "application/json" 127 | ContentTypeText = "text/plain; charset=utf-8" 128 | ContentTypeHTML = "text/html; charset=utf-8" 129 | ) 130 | 131 | const ( 132 | HeaderContentType = "Content-Type" 133 | HeaderContentLength = "Content-Length" 134 | HeaderLocation = "Location" 135 | HeaderTransferEncoding = "Transfer-Encoding" 136 | ) 137 | -------------------------------------------------------------------------------- /crates/nylon-tls/src/metrics.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use dashmap::DashMap; 3 | use std::sync::Arc; 4 | use std::sync::atomic::{AtomicU64, Ordering}; 5 | 6 | /// ACME metrics สำหรับ monitoring 7 | #[derive(Debug, Clone)] 8 | pub struct AcmeMetrics { 9 | /// จำนวนครั้งที่ issue certificate สำเร็จ 10 | pub issuance_success: Arc, 11 | /// จำนวนครั้งที่ issue certificate ล้มเหลว 12 | pub issuance_failure: Arc, 13 | /// จำนวนครั้งที่ renew certificate สำเร็จ 14 | pub renewal_success: Arc, 15 | /// จำนวนครั้งที่ renew certificate ล้มเหลว 16 | pub renewal_failure: Arc, 17 | /// จำนวนครั้งที่ challenge validation สำเร็จ 18 | pub challenge_success: Arc, 19 | /// จำนวนครั้งที่ challenge validation ล้มเหลว 20 | pub challenge_failure: Arc, 21 | /// Domain-specific metrics 22 | pub domain_metrics: Arc>, 23 | } 24 | 25 | /// Metrics สำหรับแต่ละ domain 26 | #[derive(Debug, Clone)] 27 | pub struct DomainMetrics { 28 | pub domain: String, 29 | pub last_issuance: Option>, 30 | pub last_renewal: Option>, 31 | pub last_failure: Option>, 32 | pub failure_count: u32, 33 | pub days_until_expiry: i64, 34 | } 35 | 36 | impl Default for AcmeMetrics { 37 | fn default() -> Self { 38 | Self::new() 39 | } 40 | } 41 | 42 | impl AcmeMetrics { 43 | pub fn new() -> Self { 44 | Self { 45 | issuance_success: Arc::new(AtomicU64::new(0)), 46 | issuance_failure: Arc::new(AtomicU64::new(0)), 47 | renewal_success: Arc::new(AtomicU64::new(0)), 48 | renewal_failure: Arc::new(AtomicU64::new(0)), 49 | challenge_success: Arc::new(AtomicU64::new(0)), 50 | challenge_failure: Arc::new(AtomicU64::new(0)), 51 | domain_metrics: Arc::new(DashMap::new()), 52 | } 53 | } 54 | 55 | /// บันทึก issuance success 56 | pub fn record_issuance_success(&self, domain: &str) { 57 | self.issuance_success.fetch_add(1, Ordering::Relaxed); 58 | 59 | let mut metrics = self 60 | .domain_metrics 61 | .entry(domain.to_string()) 62 | .or_insert_with(|| DomainMetrics { 63 | domain: domain.to_string(), 64 | last_issuance: None, 65 | last_renewal: None, 66 | last_failure: None, 67 | failure_count: 0, 68 | days_until_expiry: 0, 69 | }); 70 | 71 | metrics.last_issuance = Some(Utc::now()); 72 | metrics.failure_count = 0; // Reset failure count on success 73 | } 74 | 75 | /// บันทึก issuance failure 76 | pub fn record_issuance_failure(&self, domain: &str) { 77 | self.issuance_failure.fetch_add(1, Ordering::Relaxed); 78 | 79 | let mut metrics = self 80 | .domain_metrics 81 | .entry(domain.to_string()) 82 | .or_insert_with(|| DomainMetrics { 83 | domain: domain.to_string(), 84 | last_issuance: None, 85 | last_renewal: None, 86 | last_failure: None, 87 | failure_count: 0, 88 | days_until_expiry: 0, 89 | }); 90 | 91 | metrics.last_failure = Some(Utc::now()); 92 | metrics.failure_count += 1; 93 | } 94 | 95 | /// บันทึก renewal success 96 | pub fn record_renewal_success(&self, domain: &str) { 97 | self.renewal_success.fetch_add(1, Ordering::Relaxed); 98 | 99 | let mut metrics = self 100 | .domain_metrics 101 | .entry(domain.to_string()) 102 | .or_insert_with(|| DomainMetrics { 103 | domain: domain.to_string(), 104 | last_issuance: None, 105 | last_renewal: None, 106 | last_failure: None, 107 | failure_count: 0, 108 | days_until_expiry: 0, 109 | }); 110 | 111 | metrics.last_renewal = Some(Utc::now()); 112 | metrics.failure_count = 0; // Reset failure count on success 113 | } 114 | 115 | /// บันทึก renewal failure 116 | pub fn record_renewal_failure(&self, domain: &str) { 117 | self.renewal_failure.fetch_add(1, Ordering::Relaxed); 118 | 119 | let mut metrics = self 120 | .domain_metrics 121 | .entry(domain.to_string()) 122 | .or_insert_with(|| DomainMetrics { 123 | domain: domain.to_string(), 124 | last_issuance: None, 125 | last_renewal: None, 126 | last_failure: None, 127 | failure_count: 0, 128 | days_until_expiry: 0, 129 | }); 130 | 131 | metrics.last_failure = Some(Utc::now()); 132 | metrics.failure_count += 1; 133 | } 134 | 135 | /// อัพเดท days until expiry 136 | pub fn update_days_until_expiry(&self, domain: &str, days: i64) { 137 | let mut metrics = self 138 | .domain_metrics 139 | .entry(domain.to_string()) 140 | .or_insert_with(|| DomainMetrics { 141 | domain: domain.to_string(), 142 | last_issuance: None, 143 | last_renewal: None, 144 | last_failure: None, 145 | failure_count: 0, 146 | days_until_expiry: days, 147 | }); 148 | 149 | metrics.days_until_expiry = days; 150 | } 151 | 152 | /// ดึง metrics ทั้งหมด 153 | pub fn get_summary(&self) -> MetricsSummary { 154 | MetricsSummary { 155 | issuance_success: self.issuance_success.load(Ordering::Relaxed), 156 | issuance_failure: self.issuance_failure.load(Ordering::Relaxed), 157 | renewal_success: self.renewal_success.load(Ordering::Relaxed), 158 | renewal_failure: self.renewal_failure.load(Ordering::Relaxed), 159 | challenge_success: self.challenge_success.load(Ordering::Relaxed), 160 | challenge_failure: self.challenge_failure.load(Ordering::Relaxed), 161 | domain_count: self.domain_metrics.len(), 162 | } 163 | } 164 | } 165 | 166 | /// สรุป metrics 167 | #[derive(Debug, Clone)] 168 | pub struct MetricsSummary { 169 | pub issuance_success: u64, 170 | pub issuance_failure: u64, 171 | pub renewal_success: u64, 172 | pub renewal_failure: u64, 173 | pub challenge_success: u64, 174 | pub challenge_failure: u64, 175 | pub domain_count: usize, 176 | } 177 | -------------------------------------------------------------------------------- /crates/nylon/src/runtime.rs: -------------------------------------------------------------------------------- 1 | //! Nylon Runtime Server Implementation 2 | //! 3 | //! This module contains the core runtime functionality for the Nylon proxy server, 4 | //! including server initialization, configuration, and service management. 5 | 6 | use crate::{background_service::NylonBackgroundService, dynamic_certificate::new_tls_settings}; 7 | use nylon_config::runtime::RuntimeConfig; 8 | use nylon_error::NylonError; 9 | use pingora::{ 10 | prelude::{Opt, background_service}, 11 | proxy, 12 | server::{Server, configuration::ServerConf}, 13 | }; 14 | use tracing::info; 15 | 16 | /// Nylon runtime server instance 17 | #[derive(Debug, Clone)] 18 | pub struct NylonRuntime {} 19 | 20 | impl NylonRuntime { 21 | /// Create a new Nylon server instance 22 | /// 23 | /// This method initializes the Pingora server with Nylon-specific configuration 24 | /// including HTTP/HTTPS listeners, TLS settings, and background services. 25 | /// 26 | /// # Returns 27 | /// 28 | /// * `Result` - The configured server instance or an error 29 | pub fn new_server() -> Result { 30 | let config = RuntimeConfig::get()?; 31 | info!("Initializing Nylon server with configuration"); 32 | 33 | // Create Pingora server with basic options 34 | let opt = Opt { 35 | daemon: config.pingora.daemon, 36 | ..Default::default() 37 | }; 38 | 39 | let mut pingora_server = 40 | Server::new(Some(opt)).map_err(|e| NylonError::PingoraError(e.to_string()))?; 41 | 42 | // Configure server settings 43 | let conf = create_server_config(&config)?; 44 | pingora_server.configuration = conf.into(); 45 | 46 | let runtime = NylonRuntime {}; 47 | 48 | // Add HTTP service 49 | add_http_service(&mut pingora_server, &config, &runtime)?; 50 | 51 | // Add HTTPS service if configured 52 | if !config.https.is_empty() { 53 | add_https_service(&mut pingora_server, &config, &runtime)?; 54 | } 55 | 56 | // Add background service 57 | let bg_service = background_service("NylonBackgroundService", NylonBackgroundService {}); 58 | pingora_server.add_service(bg_service); 59 | 60 | info!("Nylon server initialization completed successfully"); 61 | Ok(pingora_server) 62 | } 63 | } 64 | 65 | /// Create server configuration from runtime config 66 | /// 67 | /// # Arguments 68 | /// 69 | /// * `config` - The runtime configuration 70 | /// 71 | /// # Returns 72 | /// 73 | /// * `Result` - The server configuration 74 | fn create_server_config(config: &RuntimeConfig) -> Result { 75 | let mut conf = ServerConf { 76 | daemon: config.pingora.daemon, 77 | grace_period_seconds: Some(config.pingora.grace_period_seconds), 78 | graceful_shutdown_timeout_seconds: Some(config.pingora.graceful_shutdown_timeout_seconds), 79 | threads: config.pingora.threads, 80 | ..Default::default() 81 | }; 82 | 83 | // Helper function to convert PathBuf to Option 84 | let path_to_string = |path: &std::path::PathBuf| -> Option { 85 | path.to_str().filter(|s| !s.is_empty()).map(String::from) 86 | }; 87 | 88 | // Set optional configuration values 89 | if let Some(v) = &config.pingora.error_log { 90 | conf.error_log = path_to_string(v); 91 | } 92 | if let Some(v) = &config.pingora.pid_file { 93 | conf.pid_file = path_to_string(v).unwrap_or_default(); 94 | } 95 | if let Some(v) = &config.pingora.upgrade_sock { 96 | conf.upgrade_sock = path_to_string(v).unwrap_or_default(); 97 | } 98 | if let Some(v) = &config.pingora.ca_file { 99 | conf.ca_file = path_to_string(v); 100 | } 101 | 102 | // Set user and group if provided 103 | conf.user = config.pingora.user.clone().filter(|s| !s.is_empty()); 104 | conf.group = config.pingora.group.clone().filter(|s| !s.is_empty()); 105 | 106 | // Set work stealing if configured 107 | conf.work_stealing = config.pingora.work_stealing.unwrap_or(conf.work_stealing); 108 | 109 | // Set upstream keepalive pool size if configured 110 | if let Some(v) = &config.pingora.upstream_keepalive_pool_size { 111 | conf.upstream_keepalive_pool_size = *v 112 | } 113 | 114 | Ok(conf) 115 | } 116 | 117 | /// Add HTTP service to the server 118 | /// 119 | /// # Arguments 120 | /// 121 | /// * `server` - The Pingora server instance 122 | /// * `config` - The runtime configuration 123 | /// * `runtime` - The Nylon runtime instance 124 | /// 125 | /// # Returns 126 | /// 127 | /// * `Result<(), NylonError>` - Success or error 128 | fn add_http_service( 129 | server: &mut Server, 130 | config: &RuntimeConfig, 131 | runtime: &NylonRuntime, 132 | ) -> Result<(), NylonError> { 133 | let mut pingora_svc = proxy::http_proxy_service(&server.configuration, runtime.clone()); 134 | 135 | // Find and add zero address first (for binding to all interfaces) 136 | if let Some(http_zero_addr) = config.http.iter().find(|a| a.contains("0.0.0.0")) { 137 | pingora_svc.add_tcp(http_zero_addr); 138 | info!("HTTP proxy server started on http://{}", http_zero_addr); 139 | } else { 140 | // Add all configured HTTP addresses 141 | for addr in &config.http { 142 | pingora_svc.add_tcp(addr); 143 | info!("HTTP proxy server started on http://{}", addr); 144 | } 145 | } 146 | 147 | server.add_service(pingora_svc); 148 | Ok(()) 149 | } 150 | 151 | /// Add HTTPS service to the server 152 | /// 153 | /// # Arguments 154 | /// 155 | /// * `server` - The Pingora server instance 156 | /// * `config` - The runtime configuration 157 | /// * `runtime` - The Nylon runtime instance 158 | /// 159 | /// # Returns 160 | /// 161 | /// * `Result<(), NylonError>` - Success or error 162 | fn add_https_service( 163 | server: &mut Server, 164 | config: &RuntimeConfig, 165 | runtime: &NylonRuntime, 166 | ) -> Result<(), NylonError> { 167 | let mut pingora_svc = proxy::http_proxy_service(&server.configuration, runtime.clone()); 168 | 169 | // Create TLS settings 170 | let tls_settings = new_tls_settings()?; 171 | 172 | // Find and add zero address first (for binding to all interfaces) 173 | if let Some(https_zero_addr) = config.https.iter().find(|a| a.contains("0.0.0.0")) { 174 | pingora_svc.add_tls_with_settings(https_zero_addr, None, tls_settings); 175 | info!("HTTPS proxy server started on https://{}", https_zero_addr); 176 | } else { 177 | // Add all configured HTTPS addresses 178 | for addr in &config.https { 179 | let tls_settings = new_tls_settings()?; 180 | pingora_svc.add_tls_with_settings(addr, None, tls_settings); 181 | info!("HTTPS proxy server started on https://{}", addr); 182 | } 183 | } 184 | 185 | server.add_service(pingora_svc); 186 | Ok(()) 187 | } 188 | -------------------------------------------------------------------------------- /crates/nylon-config/src/runtime.rs: -------------------------------------------------------------------------------- 1 | use nylon_error::NylonError; 2 | use nylon_types::websocket::WebSocketAdapterConfig; 3 | use serde::{Deserialize, Serialize}; 4 | use std::{path::PathBuf, str::FromStr}; 5 | 6 | const DEFAULT_NYLON_DIR: &str = "/etc/nylon"; 7 | 8 | fn default_config_dir() -> PathBuf { 9 | PathBuf::from(format!("{}/config", DEFAULT_NYLON_DIR)) 10 | } 11 | 12 | fn default_acme_dir() -> PathBuf { 13 | PathBuf::from(format!("{}/acme", DEFAULT_NYLON_DIR)) 14 | } 15 | 16 | fn default_daemon() -> bool { 17 | false 18 | } 19 | 20 | fn default_threads() -> usize { 21 | let cpus = num_cpus::get(); 22 | let reserved = if cpus >= 6 { 23 | 2 24 | } else if cpus > 1 { 25 | 1 26 | } else { 27 | 0 28 | }; 29 | (cpus - reserved).clamp(1, 16) 30 | } 31 | 32 | fn default_grace_period() -> u64 { 33 | 60 34 | } 35 | 36 | fn default_shutdown_timeout() -> u64 { 37 | 10 38 | } 39 | 40 | #[derive(Debug, Serialize, Deserialize, Clone)] 41 | pub struct RuntimeConfig { 42 | /// HTTP listening addresses 43 | #[serde(default)] 44 | pub http: Vec, 45 | 46 | /// HTTPS listening addresses 47 | #[serde(default)] 48 | pub https: Vec, 49 | 50 | /// Prometheus metrics addresses 51 | #[serde(default)] 52 | pub metrics: Vec, 53 | 54 | /// Path to directory containing service and route definitions 55 | #[serde(default = "default_config_dir")] 56 | pub config_dir: PathBuf, 57 | 58 | /// Path to directory containing ACME certificates 59 | #[serde(default = "default_acme_dir")] 60 | pub acme: PathBuf, 61 | 62 | /// Pingora runtime configuration 63 | #[serde(default)] 64 | pub pingora: PingoraConfig, 65 | 66 | /// WebSocket adapter configuration 67 | #[serde(default)] 68 | pub websocket: Option, 69 | } 70 | 71 | #[derive(Debug, Serialize, Deserialize, Clone)] 72 | pub struct PingoraConfig { 73 | /// Run in daemon mode 74 | #[serde(default = "default_daemon")] 75 | pub daemon: bool, 76 | 77 | /// Number of worker threads 78 | #[serde(default = "default_threads")] 79 | pub threads: usize, 80 | 81 | /// Grace period for in-flight connections 82 | #[serde(default = "default_grace_period")] 83 | pub grace_period_seconds: u64, 84 | 85 | /// Maximum wait time before forced shutdown 86 | #[serde(default = "default_shutdown_timeout")] 87 | pub graceful_shutdown_timeout_seconds: u64, 88 | 89 | /// Max number of upstream keepalive connections 90 | #[serde(default)] 91 | pub upstream_keepalive_pool_size: Option, 92 | 93 | /// Enable work stealing between threads 94 | #[serde(default)] 95 | pub work_stealing: Option, 96 | 97 | /// File path for error logging 98 | #[serde(default)] 99 | pub error_log: Option, 100 | 101 | /// File path for PID file 102 | #[serde(default)] 103 | pub pid_file: Option, 104 | 105 | /// Socket path for zero-downtime upgrade 106 | #[serde(default)] 107 | pub upgrade_sock: Option, 108 | 109 | /// User to drop privileges to 110 | #[serde(default)] 111 | pub user: Option, 112 | 113 | /// Group to drop privileges to 114 | #[serde(default)] 115 | pub group: Option, 116 | 117 | /// Path to trusted CA certificates 118 | #[serde(default)] 119 | pub ca_file: Option, 120 | } 121 | 122 | impl Default for RuntimeConfig { 123 | fn default() -> Self { 124 | Self { 125 | http: vec![], 126 | https: vec![], 127 | metrics: vec![], 128 | config_dir: default_config_dir(), 129 | acme: default_acme_dir(), 130 | pingora: PingoraConfig::default(), 131 | websocket: None, 132 | } 133 | } 134 | } 135 | 136 | impl Default for PingoraConfig { 137 | fn default() -> Self { 138 | Self { 139 | daemon: default_daemon(), 140 | threads: default_threads(), 141 | grace_period_seconds: default_grace_period(), 142 | graceful_shutdown_timeout_seconds: default_shutdown_timeout(), 143 | upstream_keepalive_pool_size: None, 144 | work_stealing: None, 145 | error_log: None, 146 | pid_file: None, 147 | upgrade_sock: None, 148 | user: None, 149 | group: None, 150 | ca_file: None, 151 | } 152 | } 153 | } 154 | 155 | impl FromStr for RuntimeConfig { 156 | type Err = NylonError; 157 | 158 | /// Parse the runtime config from a string 159 | /// 160 | /// # Arguments 161 | /// 162 | /// * `s` - The string to parse 163 | /// 164 | /// # Returns 165 | fn from_str(s: &str) -> Result { 166 | serde_yaml_ng::from_str(s).map_err(|e| NylonError::ConfigError(e.to_string())) 167 | } 168 | } 169 | 170 | impl RuntimeConfig { 171 | /// Load the runtime config from a file 172 | /// 173 | /// # Arguments 174 | /// 175 | /// * `path` - The path to the config file 176 | /// 177 | /// # Returns 178 | /// 179 | /// * `Result` - The result of the operation 180 | pub fn from_file(path: &str) -> Result { 181 | let content = 182 | std::fs::read_to_string(path).map_err(|e| NylonError::ConfigError(e.to_string()))?; 183 | Self::from_str(&content) 184 | } 185 | 186 | /// Store the runtime config in the store 187 | /// 188 | /// # Returns 189 | /// 190 | /// * `Result<(), NylonError>` - The result of the operation 191 | pub fn store(&self) -> Result<(), NylonError> { 192 | nylon_store::insert(nylon_store::KEY_RUNTIME_CONFIG, self.clone()); 193 | Ok(()) 194 | } 195 | 196 | /// Get the runtime config from the store 197 | /// 198 | /// # Returns 199 | /// 200 | /// * `Result` - The result of the operation 201 | pub fn get() -> Result { 202 | nylon_store::get(nylon_store::KEY_RUNTIME_CONFIG).ok_or(NylonError::ConfigError( 203 | "Runtime config not found".to_string(), 204 | )) 205 | } 206 | } 207 | 208 | #[cfg(test)] 209 | mod tests { 210 | use super::*; 211 | 212 | #[test] 213 | fn test_parse_config() { 214 | let yaml = r#" 215 | http: 216 | - "127.0.0.1:80" 217 | - "10.10.0.3:80" 218 | https: 219 | - "127.0.0.1:443" 220 | - "10.10.0.3:443" 221 | metrics: 222 | - "10.10.0.3:6192" 223 | config_dir: /etc/nylon/config 224 | debug: true 225 | pingora: 226 | daemon: true 227 | threads: 6 228 | grace_period_seconds: 60 229 | graceful_shutdown_timeout_seconds: 10 230 | "#; 231 | 232 | let config = RuntimeConfig::from_str(yaml).unwrap(); 233 | assert_eq!(config.http.len(), 2); 234 | assert_eq!(config.https.len(), 2); 235 | assert_eq!(config.metrics.len(), 1); 236 | assert_eq!(config.config_dir.to_str().unwrap(), "/etc/nylon/config"); 237 | assert!(config.pingora.daemon); 238 | assert_eq!(config.pingora.threads, 6); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /crates/nylon-store/src/tls.rs: -------------------------------------------------------------------------------- 1 | use crate::{KEY_ACME_CERTS, KEY_TLS, get, insert}; 2 | use lru::LruCache; 3 | use nylon_error::NylonError; 4 | use nylon_tls::CertificateInfo; 5 | use nylon_types::tls::{TlsConfig, TlsKind}; 6 | use once_cell::sync::Lazy; 7 | use std::collections::HashMap; 8 | use std::num::NonZeroUsize; 9 | use std::sync::Mutex; 10 | 11 | // LRU cache for TLS certificate lookups - cache up to 1,000 domains 12 | static TLS_CERT_CACHE: Lazy>> = 13 | Lazy::new(|| Mutex::new(LruCache::new(NonZeroUsize::new(1_000).unwrap()))); 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct TlsStore { 17 | pub cert: Vec, 18 | pub key: Vec, 19 | pub chain: Vec>, 20 | } 21 | 22 | pub fn store(tls: Vec<&TlsConfig>, acme_dir: Option) -> Result<(), NylonError> { 23 | let mut tls_store = HashMap::new(); 24 | let mut acme_configs = HashMap::new(); 25 | 26 | for t in tls { 27 | match t.kind { 28 | TlsKind::Custom => { 29 | // store custom tls 30 | let Some(path_cert) = &t.cert else { 31 | return Err(NylonError::ConfigError( 32 | "Custom TLS certificate path is required".to_string(), 33 | )); 34 | }; 35 | let Some(path_key) = &t.key else { 36 | return Err(NylonError::ConfigError( 37 | "Custom TLS key path is required".to_string(), 38 | )); 39 | }; 40 | let cert = 41 | std::fs::read(path_cert).map_err(|e| NylonError::ConfigError(e.to_string()))?; 42 | let key = 43 | std::fs::read(path_key).map_err(|e| NylonError::ConfigError(e.to_string()))?; 44 | let mut chain = vec![]; 45 | if let Some(chain_path) = &t.chain { 46 | for path in chain_path { 47 | let cert = std::fs::read(path) 48 | .map_err(|e| NylonError::ConfigError(e.to_string()))?; 49 | chain.push(cert); 50 | } 51 | } 52 | for domain in t.domains.clone() { 53 | tls_store.insert( 54 | domain, 55 | TlsStore { 56 | cert: cert.clone(), 57 | key: key.clone(), 58 | chain: chain.clone(), 59 | }, 60 | ); 61 | } 62 | } 63 | TlsKind::Acme => { 64 | // เก็บ ACME config สำหรับแต่ละ domain 65 | // รองรับทั้ง nested (acme:) และ flat format 66 | let acme_config = t.acme.clone().or_else(|| t.acme_flat.clone()); 67 | if let Some(mut acme_config) = acme_config { 68 | // ตั้งค่า acme_dir จากที่ส่งมา 69 | if acme_config.acme_dir.is_none() { 70 | acme_config.acme_dir = acme_dir.clone(); 71 | } 72 | for domain in &t.domains { 73 | acme_configs.insert(domain.clone(), acme_config.clone()); 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | insert::>(KEY_TLS, tls_store); 81 | crate::insert(crate::KEY_ACME_CONFIG, acme_configs); 82 | 83 | // Initialize ACME certificates store only if it doesn't exist 84 | // Don't overwrite existing certificates on reload 85 | if get::>(KEY_ACME_CERTS).is_none() { 86 | insert::>(KEY_ACME_CERTS, HashMap::new()); 87 | } 88 | 89 | // Clear TLS cert cache when configuration is reloaded 90 | clear_tls_cert_cache(); 91 | 92 | Ok(()) 93 | } 94 | 95 | pub fn get_certs(domain: &str) -> Result { 96 | // Check cache first 97 | if let Ok(mut cache) = TLS_CERT_CACHE.lock() 98 | && let Some(cached) = cache.get(domain) 99 | { 100 | tracing::debug!("TLS cert cache hit: {}", domain); 101 | return Ok(cached.clone()); 102 | } 103 | 104 | tracing::debug!("TLS cert cache miss: {}", domain); 105 | 106 | // Cache miss - lookup certificate 107 | let cert_store = lookup_cert_from_store(domain)?; 108 | 109 | // Store in cache 110 | if let Ok(mut cache) = TLS_CERT_CACHE.lock() { 111 | cache.put(domain.to_string(), cert_store.clone()); 112 | } 113 | 114 | Ok(cert_store) 115 | } 116 | 117 | /// Internal function to lookup certificate from store (without cache) 118 | fn lookup_cert_from_store(domain: &str) -> Result { 119 | // ลองหาจาก ACME certificates ก่อน 120 | if let Some(acme_certs) = get::>(KEY_ACME_CERTS) 121 | && let Some(cert_info) = acme_certs.get(domain) 122 | { 123 | return Ok(TlsStore { 124 | cert: cert_info.cert.clone(), 125 | key: cert_info.key.clone(), 126 | chain: cert_info.chain.clone(), 127 | }); 128 | } 129 | 130 | // ถ้าไม่มีใน ACME ให้หาจาก custom certificates 131 | let tls_store = get::>(KEY_TLS).ok_or(NylonError::ConfigError( 132 | format!("TLS domain {} not found", domain), 133 | ))?; 134 | let tls_store = tls_store 135 | .get(domain) 136 | .ok_or(NylonError::ConfigError(format!( 137 | "TLS domain {} not found", 138 | domain 139 | )))?; 140 | Ok(tls_store.clone()) 141 | } 142 | 143 | /// เก็บ ACME certificate 144 | pub fn store_acme_cert(cert_info: CertificateInfo) -> Result<(), NylonError> { 145 | let mut acme_certs = 146 | get::>(KEY_ACME_CERTS).unwrap_or_default(); 147 | 148 | let domain = cert_info.domain.clone(); 149 | acme_certs.insert(domain.clone(), cert_info); 150 | insert::>(KEY_ACME_CERTS, acme_certs); 151 | 152 | // Invalidate cache for this domain 153 | if let Ok(mut cache) = TLS_CERT_CACHE.lock() { 154 | cache.pop(&domain); 155 | tracing::info!("Invalidated TLS cert cache for domain: {}", domain); 156 | } 157 | 158 | Ok(()) 159 | } 160 | 161 | /// Clear TLS certificate cache - useful when certificates are reloaded 162 | pub fn clear_tls_cert_cache() { 163 | if let Ok(mut cache) = TLS_CERT_CACHE.lock() { 164 | cache.clear(); 165 | tracing::info!("TLS certificate cache cleared"); 166 | } 167 | } 168 | 169 | /// Get TLS certificate cache statistics 170 | pub fn get_tls_cert_cache_stats() -> (usize, usize) { 171 | if let Ok(cache) = TLS_CERT_CACHE.lock() { 172 | (cache.len(), cache.cap().get()) 173 | } else { 174 | (0, 0) 175 | } 176 | } 177 | 178 | /// ดึงรายการ certificates ทั้งหมดที่ต้องตรวจสอบการ renew 179 | pub fn get_all_certificates() -> Vec { 180 | let acme_certs = get::>(KEY_ACME_CERTS).unwrap_or_default(); 181 | 182 | acme_certs.values().cloned().collect() 183 | } 184 | -------------------------------------------------------------------------------- /crates/nylon-store/src/websockets.rs: -------------------------------------------------------------------------------- 1 | use crate::websocket_adapter::{MemoryAdapter, WebSocketAdapter}; 2 | use dashmap::DashMap; 3 | use nylon_error::NylonError; 4 | use nylon_types::websocket::{ 5 | AdapterType, WebSocketAdapterConfig, WebSocketConnection, WebSocketEvent, WebSocketMessage, 6 | }; 7 | use once_cell::sync::Lazy; 8 | use std::sync::Arc; 9 | use tokio::sync::RwLock; 10 | use tokio::sync::mpsc::UnboundedSender; 11 | 12 | // WebSocket related constants 13 | 14 | // RFC 6455 GUID used to compute Sec-WebSocket-Accept 15 | pub const WEBSOCKET_GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 16 | 17 | // Global WebSocket adapter instance 18 | static WEBSOCKET_ADAPTER: Lazy>>> = 19 | Lazy::new(|| RwLock::new(None)); 20 | 21 | // Local connection senders to push messages to active sessions 22 | static LOCAL_SENDERS: Lazy>> = 23 | Lazy::new(DashMap::new); 24 | 25 | /// Initialize WebSocket adapter with configuration 26 | pub async fn initialize_adapter(config: Option) -> Result<(), NylonError> { 27 | let adapter: Arc = match config { 28 | Some(config) => match config.adapter_type { 29 | AdapterType::Memory => Arc::new(MemoryAdapter::new()) as Arc, 30 | AdapterType::Redis => { 31 | let redis_config = config.redis.ok_or_else(|| { 32 | NylonError::ConfigError( 33 | "Redis configuration required for Redis adapter".to_string(), 34 | ) 35 | })?; 36 | 37 | use crate::redis_adapter::RedisAdapter; 38 | Arc::new(RedisAdapter::new(redis_config).await?) as Arc 39 | } 40 | AdapterType::Cluster => { 41 | // For now, cluster uses Redis adapter 42 | let redis_config = config.redis.ok_or_else(|| { 43 | NylonError::ConfigError( 44 | "Redis configuration required for Cluster adapter".to_string(), 45 | ) 46 | })?; 47 | 48 | use crate::redis_adapter::RedisAdapter; 49 | Arc::new(RedisAdapter::new(redis_config).await?) as Arc 50 | } 51 | }, 52 | None => Arc::new(MemoryAdapter::new()) as Arc, 53 | }; 54 | 55 | let mut global_adapter = WEBSOCKET_ADAPTER.write().await; 56 | // Start cluster event dispatcher if adapter provides receiver 57 | if let Some(mut rx) = adapter.get_event_receiver() { 58 | tokio::spawn(async move { 59 | while let Some(event) = rx.recv().await { 60 | match event { 61 | WebSocketEvent::SendToConnection { 62 | connection_id, 63 | message, 64 | .. 65 | } => { 66 | if let Some(sender) = LOCAL_SENDERS.get(&connection_id) { 67 | let _ = sender.send(message); 68 | } 69 | } 70 | WebSocketEvent::BroadcastToRoom { 71 | room, 72 | message, 73 | exclude_connection, 74 | .. 75 | } => { 76 | if let Ok(connections) = get_room_connections(&room).await { 77 | for cid in connections { 78 | if let Some(exclude) = &exclude_connection 79 | && &cid == exclude 80 | { 81 | continue; 82 | } 83 | if let Some(sender) = LOCAL_SENDERS.get(&cid) { 84 | let _ = sender.send(message.clone()); 85 | } 86 | } 87 | } 88 | } 89 | _ => {} 90 | } 91 | } 92 | }); 93 | } 94 | *global_adapter = Some(adapter); 95 | 96 | Ok(()) 97 | } 98 | 99 | /// Get the global WebSocket adapter 100 | pub async fn get_adapter() -> Result, NylonError> { 101 | let adapter_guard = WEBSOCKET_ADAPTER.read().await; 102 | adapter_guard 103 | .as_ref() 104 | .ok_or_else(|| NylonError::ConfigError("WebSocket adapter not initialized".to_string())) 105 | .cloned() 106 | } 107 | 108 | /// Add a WebSocket connection 109 | pub async fn add_connection(connection: WebSocketConnection) -> Result<(), NylonError> { 110 | let adapter = get_adapter().await?; 111 | adapter.add_connection(connection).await 112 | } 113 | 114 | /// Remove a WebSocket connection 115 | pub async fn remove_connection(connection_id: &str) -> Result<(), NylonError> { 116 | let adapter = get_adapter().await?; 117 | adapter.remove_connection(connection_id).await 118 | } 119 | 120 | /// Join a connection to a room 121 | pub async fn join_room(connection_id: &str, room: &str) -> Result<(), NylonError> { 122 | let adapter = get_adapter().await?; 123 | adapter.join_room(connection_id, room).await 124 | } 125 | 126 | /// Leave a connection from a room 127 | pub async fn leave_room(connection_id: &str, room: &str) -> Result<(), NylonError> { 128 | let adapter = get_adapter().await?; 129 | adapter.leave_room(connection_id, room).await 130 | } 131 | 132 | /// Broadcast message to all connections in a room 133 | pub async fn broadcast_to_room( 134 | room: &str, 135 | message: WebSocketMessage, 136 | exclude_connection: Option<&str>, 137 | ) -> Result<(), NylonError> { 138 | let adapter = get_adapter().await?; 139 | adapter 140 | .broadcast_to_room(room, message, exclude_connection) 141 | .await 142 | } 143 | 144 | /// Send message to a specific connection 145 | pub async fn send_to_connection( 146 | connection_id: &str, 147 | message: WebSocketMessage, 148 | ) -> Result<(), NylonError> { 149 | let adapter = get_adapter().await?; 150 | adapter.send_to_connection(connection_id, message).await 151 | } 152 | 153 | /// Get all connections in a room 154 | pub async fn get_room_connections(room: &str) -> Result, NylonError> { 155 | let adapter = get_adapter().await?; 156 | adapter.get_room_connections(room).await 157 | } 158 | 159 | /// Get all rooms for a connection 160 | pub async fn get_connection_rooms(connection_id: &str) -> Result, NylonError> { 161 | let adapter = get_adapter().await?; 162 | adapter.get_connection_rooms(connection_id).await 163 | } 164 | 165 | /// Register a local sender for a connection to receive cluster messages 166 | pub fn register_local_sender(connection_id: String, sender: UnboundedSender) { 167 | LOCAL_SENDERS.insert(connection_id, sender); 168 | } 169 | 170 | /// Unregister a local sender when a connection closes 171 | pub fn unregister_local_sender(connection_id: &str) { 172 | LOCAL_SENDERS.remove(connection_id); 173 | } 174 | 175 | /// Get current node id from adapter 176 | pub async fn get_node_id() -> Result { 177 | let adapter = get_adapter().await?; 178 | Ok(adapter.get_node_id()) 179 | } 180 | -------------------------------------------------------------------------------- /examples/go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "C" 4 | import ( 5 | "fmt" 6 | 7 | "github.com/AssetsArt/nylon/sdk/go/sdk" 8 | ) 9 | 10 | type PluginConfig struct { 11 | Debug bool `json:"debug"` 12 | } 13 | 14 | func main() {} 15 | func init() { 16 | 17 | // Create a new plugin 18 | plugin := sdk.NewNylonPlugin() 19 | 20 | // Register initialize handler 21 | plugin.Initialize(sdk.NewInitializer(func(config PluginConfig) { 22 | fmt.Println("[NylonPlugin] Plugin initialized") 23 | fmt.Println("[NylonPlugin] Config: Debug", config.Debug) 24 | })) 25 | 26 | // Register shutdown handler 27 | plugin.Shutdown(func() { 28 | fmt.Println("[NylonPlugin] Plugin shutdown") 29 | }) 30 | 31 | // phase 32 | // - RequestFilter // Can return a full response 33 | // | 34 | // V 35 | // - ResponseFilter // Can modify the response headers 36 | // | 37 | // V 38 | // - ResponseBodyFilter // Can modify the response body 39 | // | 40 | // V 41 | // - Logging // Can log the request and response 42 | 43 | // Register middleware 44 | plugin.AddPhaseHandler("authz", func(phase *sdk.PhaseHandler) { 45 | fmt.Println("Start Authz[Go] sessionID", phase.SessionId) 46 | // Initialize phase state per request 47 | myPhaseState := map[string]bool{ 48 | "authz": false, 49 | } 50 | 51 | // Phase request filter 52 | phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) { 53 | fmt.Println("Authz[Go] RequestFilter sessionID", phase.SessionId) 54 | myPhaseState["authz"] = true 55 | 56 | payload := ctx.GetPayload() 57 | fmt.Println("[Authz][NylonPlugin] Payload", payload) 58 | 59 | response := ctx.Response() 60 | response.SetHeader("X-RequestFilter", "authz-1") 61 | // sleep 2 seconds 62 | // time.Sleep(2 * time.Second) 63 | // next phase 64 | ctx.Next() 65 | }) 66 | 67 | phase.ResponseFilter(func(ctx *sdk.PhaseResponseFilter) { 68 | fmt.Println("Authz[Go] ResponseFilter sessionID", phase.SessionId) 69 | ctx.SetResponseHeader("X-ResponseFilter", "authz-2") 70 | 71 | // for modify response body 72 | ctx.RemoveResponseHeader("Content-Length") 73 | ctx.SetResponseHeader("Transfer-Encoding", "chunked") 74 | ctx.Next() 75 | }) 76 | 77 | phase.ResponseBodyFilter(func(ctx *sdk.PhaseResponseBodyFilter) { 78 | fmt.Println("Authz[Go] ResponseBodyFilter sessionID", phase.SessionId) 79 | 80 | // Read response body 81 | res := ctx.Response() 82 | body := res.ReadBody() 83 | fmt.Println("Authz[Go] ResponseBody length:", len(body)) 84 | 85 | // Modify response body (example: append text) 86 | modifiedBody := append(body, []byte("\n")...) 87 | res.BodyRaw(modifiedBody) 88 | 89 | ctx.Next() 90 | }) 91 | 92 | phase.Logging(func(ctx *sdk.PhaseLogging) { 93 | fmt.Println("Authz[Go] Logging sessionID", phase.SessionId) 94 | 95 | // Access request info for logging 96 | req := ctx.Request() 97 | res := ctx.Response() 98 | 99 | // Log with all available information 100 | fmt.Printf("Authz[Go] Log: %s %s | Status: %d | ReqBytes: %d | ResBytes: %d | Duration: %dms | Host: %s | Client: %s | Timestamp: %d\n", 101 | req.Method(), 102 | req.Path(), 103 | res.Status(), 104 | req.Bytes(), 105 | res.Bytes(), 106 | res.Duration(), 107 | req.Host(), 108 | req.ClientIP(), 109 | req.Timestamp(), 110 | ) 111 | 112 | // Log error if any 113 | if err := res.Error(); err != "" { 114 | fmt.Printf("Authz[Go] Error: %s\n", err) 115 | } 116 | 117 | // Log response headers (example: show content-type) 118 | resHeaders := res.Headers() 119 | if contentType, ok := resHeaders["content-type"]; ok { 120 | fmt.Printf("Authz[Go] Content-Type: %s\n", contentType) 121 | } 122 | 123 | ctx.Next() 124 | }) 125 | 126 | }) 127 | 128 | plugin.AddPhaseHandler("stream", func(phase *sdk.PhaseHandler) { 129 | fmt.Println("Start Stream[Go] sessionID", phase.SessionId) 130 | phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) { 131 | fmt.Println("Stream[Go] RequestFilter sessionID", phase.SessionId) 132 | res := ctx.Response() 133 | // set status and headers 134 | res.SetStatus(200) 135 | res.SetHeader("Content-Type", "text/plain") 136 | 137 | // Start streaming response 138 | stream, err := res.Stream() 139 | if err != nil { 140 | fmt.Println("[Stream][NylonPlugin] Error streaming response", err) 141 | ctx.Next() 142 | return 143 | } 144 | stream.Write([]byte("Hello")) 145 | w := ", World" 146 | for i := 0; i < len(w); i++ { 147 | stream.Write([]byte(w[i : i+1])) 148 | } 149 | 150 | // End streaming response 151 | stream.End() 152 | }) 153 | }) 154 | 155 | plugin.AddPhaseHandler("myapp", func(phase *sdk.PhaseHandler) { 156 | fmt.Println("Start MyApp[Go] sessionID", phase.SessionId) 157 | phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) { 158 | fmt.Println("MyApp[Go] RequestFilter sessionID", phase.SessionId) 159 | 160 | req := ctx.Request() 161 | 162 | // Test new methods 163 | fmt.Println("MyApp[Go] URL:", req.URL()) 164 | fmt.Println("MyApp[Go] Path:", req.Path()) 165 | fmt.Println("MyApp[Go] Query:", req.Query()) 166 | fmt.Println("MyApp[Go] Params:", req.Params()) 167 | fmt.Println("MyApp[Go] Host:", req.Host()) 168 | fmt.Println("MyApp[Go] ClientIP:", req.ClientIP()) 169 | fmt.Println("MyApp[Go] Headers:", req.Headers()) 170 | 171 | res := ctx.Response() 172 | // set status and headers 173 | res.SetStatus(200) 174 | res.SetHeader("Content-Type", "application/json") 175 | res.SetHeader("Transfer-Encoding", "chunked") 176 | 177 | // Return info as JSON 178 | info := map[string]interface{}{ 179 | "url": req.URL(), 180 | "path": req.Path(), 181 | "query": req.Query(), 182 | "params": req.Params(), 183 | "host": req.Host(), 184 | "client_ip": req.ClientIP(), 185 | } 186 | res.BodyJSON(info) 187 | 188 | ctx.End() 189 | }) 190 | }) 191 | 192 | // WebSocket example 193 | plugin.AddPhaseHandler("ws", func(phase *sdk.PhaseHandler) { 194 | fmt.Println("Start WS[Go] sessionID", phase.SessionId) 195 | phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) { 196 | fmt.Println("WS[Go] RequestFilter sessionID", phase.SessionId) 197 | err := ctx.WebSocketUpgrade(sdk.WebSocketCallbacks{ 198 | OnOpen: func(ws *sdk.WebSocketConn) { 199 | fmt.Println("[WS][Go] onOpen") 200 | ws.SendText("hello from plugin") 201 | // Join default room and broadcast welcome 202 | _ = ws.JoinRoom("lobby") 203 | _ = ws.BroadcastText("lobby", "user joined") 204 | }, 205 | OnMessageText: func(ws *sdk.WebSocketConn, msg string) { 206 | fmt.Println("[WS][Go] onMessageText:", msg) 207 | ws.SendText("echo: " + msg) 208 | // Broadcast to room 209 | _ = ws.BroadcastText("lobby", msg) 210 | }, 211 | OnMessageBinary: func(ws *sdk.WebSocketConn, data []byte) { 212 | fmt.Println("[WS][Go] onMessageBinary", len(data)) 213 | ws.SendBinary(data) 214 | }, 215 | OnClose: func(ws *sdk.WebSocketConn) { 216 | fmt.Println("[WS][Go] onClose") 217 | _ = ws.LeaveRoom("lobby") 218 | }, 219 | OnError: func(ws *sdk.WebSocketConn, err string) { 220 | fmt.Println("[WS][Go] onError:", err) 221 | }, 222 | }) 223 | if err != nil { 224 | fmt.Println("[WS][Go] upgrade error:", err) 225 | // On error fallback to HTTP 226 | ctx.Next() 227 | } 228 | }) 229 | }) 230 | } 231 | -------------------------------------------------------------------------------- /docs/introduction/quick-start.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | Get up and running with Nylon in minutes. 4 | 5 | ## Installation 6 | 7 | ### From Source 8 | 9 | ```bash 10 | # Clone the repository 11 | git clone https://github.com/AssetsArt/nylon.git 12 | cd nylon 13 | 14 | # Build the project 15 | cargo build --release 16 | 17 | # The binary will be at target/release/nylon 18 | ``` 19 | 20 | ## Your First Proxy 21 | 22 | Nylon uses two configuration files: 23 | 24 | 1. **Runtime config** (`config.yaml`) - Server settings 25 | 2. **Proxy config** (in `config_dir`) - Services, routes, plugins 26 | 27 | ### 1. Create Runtime Config 28 | 29 | Create `config.yaml`: 30 | 31 | ```yaml 32 | http: 33 | - 0.0.0.0:8080 34 | 35 | config_dir: "./config" 36 | 37 | pingora: 38 | daemon: false 39 | threads: 4 40 | work_stealing: true 41 | grace_period_seconds: 60 42 | graceful_shutdown_timeout_seconds: 10 43 | ``` 44 | 45 | ### 2. Create Proxy Config 46 | 47 | Create `config/proxy.yaml`: 48 | 49 | ```yaml 50 | services: 51 | - name: backend 52 | service_type: http 53 | algorithm: round_robin 54 | endpoints: 55 | - ip: 127.0.0.1 56 | port: 3000 57 | 58 | routes: 59 | - route: 60 | type: host 61 | value: localhost 62 | name: default 63 | paths: 64 | - path: 65 | - / 66 | - /{*path} 67 | service: 68 | name: backend 69 | ``` 70 | 71 | ### 3. Start the Proxy 72 | 73 | ```bash 74 | nylon run -c config.yaml 75 | ``` 76 | 77 | ### 4. Test It 78 | 79 | ```bash 80 | curl http://localhost:8080 81 | ``` 82 | 83 | ## With HTTPS/TLS 84 | 85 | Enable HTTPS with automatic certificate management: 86 | 87 | ### Runtime Config 88 | 89 | ```yaml 90 | http: 91 | - 0.0.0.0:80 92 | https: 93 | - 0.0.0.0:443 94 | 95 | config_dir: "./config" 96 | acme: "./acme" 97 | 98 | pingora: 99 | daemon: false 100 | threads: 4 101 | ``` 102 | 103 | ### Proxy Config with TLS 104 | 105 | ```yaml 106 | tls: 107 | - type: acme 108 | provider: letsencrypt 109 | domains: 110 | - example.com 111 | acme: 112 | email: admin@example.com 113 | 114 | services: 115 | - name: backend 116 | service_type: http 117 | algorithm: round_robin 118 | endpoints: 119 | - ip: 127.0.0.1 120 | port: 3000 121 | 122 | routes: 123 | - route: 124 | type: host 125 | value: example.com 126 | name: https-route 127 | tls: 128 | enabled: true 129 | paths: 130 | - path: 131 | - / 132 | - /{*path} 133 | service: 134 | name: backend 135 | ``` 136 | 137 | ## With Plugins 138 | 139 | ### 1. Create Plugin 140 | 141 | Create `plugin.go`: 142 | 143 | ```go 144 | package main 145 | 146 | import "C" 147 | import ( 148 | "fmt" 149 | sdk "github.com/AssetsArt/nylon/sdk/go/sdk" 150 | ) 151 | 152 | func main() {} 153 | 154 | func init() { 155 | plugin := sdk.NewNylonPlugin() 156 | 157 | // Initialize handler (optional) 158 | plugin.Initialize(sdk.NewInitializer(func(config map[string]interface{}) { 159 | fmt.Println("[Plugin] Initialized") 160 | fmt.Println("[Plugin] Config:", config) 161 | })) 162 | 163 | // Shutdown handler (optional) 164 | plugin.Shutdown(func() { 165 | fmt.Println("[Plugin] Shutdown") 166 | }) 167 | 168 | // Register phase handler 169 | plugin.AddPhaseHandler("auth", func(phase *sdk.PhaseHandler) { 170 | phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) { 171 | req := ctx.Request() 172 | 173 | // Simple API key authentication 174 | apiKey := req.Header("X-API-Key") 175 | if apiKey != "secret-key" { 176 | res := ctx.Response() 177 | res.SetStatus(401) 178 | res.BodyText("Unauthorized") 179 | res.RemoveHeader("Content-Length") 180 | res.SetHeader("Transfer-Encoding", "chunked") 181 | ctx.End() 182 | return 183 | } 184 | 185 | ctx.Next() // Continue 186 | }) 187 | }) 188 | } 189 | ``` 190 | 191 | ### 2. Build Plugin 192 | 193 | ```bash 194 | go build -buildmode=c-shared -o auth.so 195 | ``` 196 | 197 | ### 3. Configure Nylon 198 | 199 | **Proxy config (`config/proxy.yaml`):** 200 | 201 | ```yaml 202 | plugins: 203 | - name: auth 204 | type: ffi 205 | file: ./auth.so 206 | config: 207 | debug: true 208 | 209 | services: 210 | - name: protected-api 211 | service_type: http 212 | algorithm: round_robin 213 | endpoints: 214 | - ip: 127.0.0.1 215 | port: 3000 216 | 217 | routes: 218 | - route: 219 | type: host 220 | value: localhost 221 | name: protected 222 | paths: 223 | - path: 224 | - / 225 | - /{*path} 226 | service: 227 | name: protected-api 228 | middleware: 229 | - plugin: auth 230 | entry: "auth" 231 | ``` 232 | 233 | ### 4. Test Protected Endpoint 234 | 235 | Without API key (should fail): 236 | ```bash 237 | curl http://localhost:8080/api 238 | # Response: 401 Unauthorized 239 | ``` 240 | 241 | With API key (should succeed): 242 | ```bash 243 | curl -H "X-API-Key: secret-key" http://localhost:8080/api 244 | # Response: forwarded to backend 245 | ``` 246 | 247 | ## Load Balancing 248 | 249 | Configure multiple backends with different algorithms: 250 | 251 | ```yaml 252 | services: 253 | # Round Robin (default) 254 | - name: api-roundrobin 255 | service_type: http 256 | algorithm: round_robin 257 | endpoints: 258 | - ip: 10.0.0.1 259 | port: 3000 260 | - ip: 10.0.0.2 261 | port: 3000 262 | - ip: 10.0.0.3 263 | port: 3000 264 | 265 | # Weighted Round Robin 266 | - name: api-weighted 267 | service_type: http 268 | algorithm: weighted 269 | endpoints: 270 | - ip: 10.0.0.1 271 | port: 3000 272 | weight: 5 273 | - ip: 10.0.0.2 274 | port: 3000 275 | weight: 3 276 | - ip: 10.0.0.3 277 | port: 3000 278 | weight: 2 279 | 280 | # Consistent Hashing 281 | - name: api-consistent 282 | service_type: http 283 | algorithm: consistent 284 | endpoints: 285 | - ip: 10.0.0.1 286 | port: 3000 287 | - ip: 10.0.0.2 288 | port: 3000 289 | - ip: 10.0.0.3 290 | port: 3000 291 | 292 | # Random 293 | - name: api-random 294 | service_type: http 295 | algorithm: random 296 | endpoints: 297 | - ip: 10.0.0.1 298 | port: 3000 299 | - ip: 10.0.0.2 300 | port: 3000 301 | ``` 302 | 303 | ## Service Types 304 | 305 | Nylon supports three types of services: 306 | 307 | ### HTTP Service 308 | Forward requests to HTTP backends: 309 | 310 | ```yaml 311 | services: 312 | - name: http-backend 313 | service_type: http 314 | algorithm: round_robin 315 | endpoints: 316 | - ip: 127.0.0.1 317 | port: 3000 318 | ``` 319 | 320 | ### Plugin Service 321 | Handle requests with Go plugins: 322 | 323 | ```yaml 324 | services: 325 | - name: custom-handler 326 | service_type: plugin 327 | plugin: 328 | name: my-plugin 329 | entry: "handler" 330 | ``` 331 | 332 | ### Static Service 333 | Serve static files: 334 | 335 | ```yaml 336 | services: 337 | - name: static-files 338 | service_type: static 339 | static: 340 | root: ./public 341 | index: index.html 342 | spa: true # SPA fallback mode 343 | ``` 344 | 345 | ## Next Steps 346 | 347 | - Learn about [Configuration](/core/configuration) in detail 348 | - Explore [Plugin Development](/plugins/overview) 349 | - Check out [Examples](/examples/basic-proxy) 350 | -------------------------------------------------------------------------------- /docs/introduction/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Install Nylon in multiple ways depending on your needs. 4 | 5 | ## Quick Install (Linux) 6 | 7 | The fastest way to install Nylon on Linux: 8 | 9 | ```bash 10 | curl -fsSL https://nylon.sh/install | bash 11 | ``` 12 | 13 | This script will: 14 | - Detect your OS and architecture (Linux x86_64/aarch64) 15 | - Detect libc variant (glibc/musl) 16 | - Download the latest release binary 17 | - Verify checksums 18 | - Install to `/usr/local/bin/nylon` (or `~/.local/bin/nylon` if no sudo) 19 | 20 | ### Supported Platforms 21 | 22 | - **Linux**: x86_64 and aarch64 23 | - **Libc**: GNU libc and musl 24 | - **macOS**: Not yet available (build from source) 25 | 26 | ### Installation Locations 27 | 28 | The installer will install to: 29 | - `/usr/local/bin/nylon` - if you have write permission or use sudo 30 | - `~/.local/bin/nylon` - if no write permission to system directories 31 | 32 | ### Verify Installation 33 | 34 | ```bash 35 | nylon --version 36 | ``` 37 | 38 | ## From Source 39 | 40 | Build Nylon from source for maximum compatibility or development: 41 | 42 | ### Prerequisites 43 | 44 | - **Rust**: 1.70 or later ([install](https://rustup.rs/)) 45 | - **Git**: To clone the repository 46 | 47 | ### Build Steps 48 | 49 | ```bash 50 | # Clone the repository 51 | git clone https://github.com/AssetsArt/nylon.git 52 | cd nylon 53 | 54 | # Build release binary 55 | cargo build --release 56 | 57 | # Binary will be at target/release/nylon 58 | ./target/release/nylon --version 59 | 60 | # Install to system (optional) 61 | sudo cp target/release/nylon /usr/local/bin/ 62 | ``` 63 | 64 | ### Build Options 65 | 66 | **Optimized build (faster binary):** 67 | ```bash 68 | RUSTFLAGS="-C target-cpu=native" cargo build --release 69 | ``` 70 | 71 | **Smaller binary (strip symbols):** 72 | ```bash 73 | cargo build --release 74 | strip target/release/nylon 75 | ``` 76 | 77 | ## Docker 78 | 79 | Run Nylon in a container: 80 | 81 | ```bash 82 | # Pull image 83 | docker pull ghcr.io/assetsart/nylon:latest 84 | 85 | # Run with config 86 | docker run -d \ 87 | --name nylon \ 88 | -p 80:8080 \ 89 | -p 443:8443 \ 90 | -v $(pwd)/config.yaml:/etc/nylon/config.yaml \ 91 | -v $(pwd)/config:/etc/nylon/config \ 92 | ghcr.io/assetsart/nylon:latest 93 | ``` 94 | 95 | ### Docker Compose 96 | 97 | Create `docker-compose.yml`: 98 | 99 | ```yaml 100 | version: '3.8' 101 | 102 | services: 103 | nylon: 104 | image: ghcr.io/assetsart/nylon:latest 105 | ports: 106 | - "80:8080" 107 | - "443:8443" 108 | - "6192:6192" # metrics 109 | volumes: 110 | - ./config.yaml:/etc/nylon/config.yaml 111 | - ./config:/etc/nylon/config 112 | - ./acme:/etc/nylon/acme 113 | restart: unless-stopped 114 | ``` 115 | 116 | Run it: 117 | ```bash 118 | docker-compose up -d 119 | ``` 120 | 121 | ## System Service (Linux) 122 | 123 | Install Nylon as a systemd service with automatic configuration: 124 | 125 | ### Install Service 126 | 127 | ```bash 128 | # Install service and create default configs 129 | sudo nylon service install 130 | ``` 131 | 132 | This creates: 133 | - `/etc/nylon/config.yaml` - Runtime configuration 134 | - `/etc/nylon/config/base.yaml` - Proxy configuration 135 | - `/etc/nylon/static/index.html` - Welcome page 136 | - `/etc/nylon/acme/` - Certificate directory 137 | - `/etc/systemd/system/nylon.service` - Systemd unit 138 | 139 | ### Service Management 140 | 141 | ```bash 142 | # Start service 143 | sudo nylon service start 144 | 145 | # Check status 146 | sudo nylon service status 147 | 148 | # Stop service 149 | sudo nylon service stop 150 | 151 | # Restart service 152 | sudo nylon service restart 153 | 154 | # Reload config (zero downtime) 155 | sudo nylon service reload 156 | 157 | # Uninstall service 158 | sudo nylon service uninstall 159 | ``` 160 | 161 | ### Verify Service 162 | 163 | ```bash 164 | # Check status 165 | sudo systemctl status nylon 166 | 167 | # View logs 168 | sudo journalctl -u nylon -f 169 | 170 | # Test endpoint 171 | curl http://localhost:8088 172 | ``` 173 | 174 | ## Verify Installation 175 | 176 | After installation, verify Nylon is working: 177 | 178 | ```bash 179 | # Check version 180 | nylon --version 181 | 182 | # Display help 183 | nylon --help 184 | 185 | # Test configuration 186 | nylon run -c config.yaml 187 | ``` 188 | 189 | ## Go SDK for Plugin Development 190 | 191 | If you want to develop Go plugins, install the SDK: 192 | 193 | ```bash 194 | # Add to your Go project 195 | go get github.com/AssetsArt/nylon/sdk/go/sdk 196 | ``` 197 | 198 | Create `plugin.go`: 199 | ```go 200 | package main 201 | 202 | import "C" 203 | import sdk "github.com/AssetsArt/nylon/sdk/go/sdk" 204 | 205 | func main() {} 206 | 207 | func init() { 208 | plugin := sdk.NewNylonPlugin() 209 | // Your plugin code here 210 | } 211 | ``` 212 | 213 | Build plugin: 214 | ```bash 215 | go build -buildmode=c-shared -o myplugin.so 216 | ``` 217 | 218 | ## Troubleshooting 219 | 220 | ### Linux: Binary not in PATH 221 | 222 | If installed to `~/.local/bin`, add to your shell profile: 223 | 224 | **Bash** (`~/.bashrc`): 225 | ```bash 226 | export PATH="$PATH:$HOME/.local/bin" 227 | ``` 228 | 229 | **Zsh** (`~/.zshrc`): 230 | ```bash 231 | export PATH="$PATH:$HOME/.local/bin" 232 | ``` 233 | 234 | Then reload: 235 | ```bash 236 | source ~/.bashrc # or ~/.zshrc 237 | ``` 238 | 239 | ### Permission Denied 240 | 241 | If you get permission errors: 242 | 243 | ```bash 244 | # Install to user directory 245 | curl -fsSL https://nylon.sh/install | bash 246 | 247 | # Or use sudo for system-wide install 248 | curl -fsSL https://nylon.sh/install | sudo bash 249 | ``` 250 | 251 | ### macOS: Build from Source 252 | 253 | macOS binaries are not available yet. Build from source: 254 | 255 | ```bash 256 | # Install Rust 257 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 258 | 259 | # Clone and build 260 | git clone https://github.com/AssetsArt/nylon.git 261 | cd nylon 262 | cargo build --release 263 | 264 | # Install 265 | sudo cp target/release/nylon /usr/local/bin/ 266 | ``` 267 | 268 | ### Checksum Verification Failed 269 | 270 | If checksum verification fails, try: 271 | 272 | ```bash 273 | # Download manually 274 | VERSION=$(curl -s https://api.github.com/repos/AssetsArt/nylon/releases/latest | grep tag_name | cut -d '"' -f 4) 275 | curl -LO "https://github.com/AssetsArt/nylon/releases/download/${VERSION}/nylon-x86_64-linux-gnu" 276 | 277 | # Verify checksum 278 | curl -LO "https://github.com/AssetsArt/nylon/releases/download/${VERSION}/linux-checksums.txt" 279 | shasum -a 256 -c linux-checksums.txt 280 | 281 | # Install 282 | chmod +x nylon-x86_64-linux-gnu 283 | sudo mv nylon-x86_64-linux-gnu /usr/local/bin/nylon 284 | ``` 285 | 286 | ## Upgrade 287 | 288 | ### Using Install Script 289 | 290 | ```bash 291 | # Re-run install script to get latest version 292 | curl -fsSL https://nylon.sh/install | bash 293 | ``` 294 | 295 | ### Manual Upgrade 296 | 297 | ```bash 298 | # Build latest from source 299 | cd nylon 300 | git pull origin main 301 | cargo build --release 302 | sudo cp target/release/nylon /usr/local/bin/ 303 | 304 | # Restart service if installed 305 | sudo nylon service restart 306 | ``` 307 | 308 | ## Uninstall 309 | 310 | ### Remove Binary 311 | 312 | ```bash 313 | # System-wide 314 | sudo rm /usr/local/bin/nylon 315 | 316 | # User install 317 | rm ~/.local/bin/nylon 318 | ``` 319 | 320 | ### Remove Service 321 | 322 | ```bash 323 | # Uninstall systemd service 324 | sudo nylon service uninstall 325 | 326 | # Remove configs (optional) 327 | sudo rm -rf /etc/nylon 328 | ``` 329 | 330 | ### Docker 331 | 332 | ```bash 333 | # Stop and remove container 334 | docker stop nylon 335 | docker rm nylon 336 | 337 | # Remove image 338 | docker rmi ghcr.io/assetsart/nylon:latest 339 | ``` 340 | 341 | ## Next Steps 342 | 343 | - [Quick Start](/introduction/quick-start) - Get started with Nylon 344 | - [Configuration](/core/configuration) - Configure Nylon 345 | - [Plugin Development](/plugins/overview) - Extend with plugins 346 | -------------------------------------------------------------------------------- /docs/core/routing.md: -------------------------------------------------------------------------------- 1 | # Routing 2 | 3 | Nylon routes requests by combining host, header, and path rules with optional rewrites and middleware. This guide walks through the most common scenarios—from the basics to more advanced matching—so you can design clear, maintainable route layouts. 4 | 5 | ## Quick Start 6 | 7 | ```yaml 8 | routes: 9 | - route: 10 | type: host 11 | value: api.example.com 12 | name: api 13 | paths: 14 | - path: /v1/{*path} 15 | service: 16 | name: api-v1 17 | - path: /{*path} 18 | service: 19 | name: fallback 20 | ``` 21 | 22 | 1. Choose a matcher (`host` or `header`) for the route. 23 | 2. Name the route so you can refer to it in logs and dashboards. 24 | 3. Describe one or more `paths` and map each to a backend service. 25 | 4. Optionally attach middleware, TLS, or rewrites per path. 26 | 27 | ## Building Blocks 28 | 29 | - **Route matcher** (`route.type`, `route.value`): Determines when the route is eligible. You can list multiple hosts separated by `|`. 30 | - **Path entry** (`paths[].path`): Checked in order to match the request path and HTTP method. 31 | - **Service block** (`service`): Points to the upstream service and optional rewrite target. 32 | - **Middleware** (`middleware` or `middleware_groups`): Attach reusable filters or plugin handlers. 33 | 34 | ## Matching Strategies 35 | 36 | ### Host-based routing 37 | 38 | ```yaml 39 | routes: 40 | - route: 41 | type: host 42 | value: api.example.com|api.internal 43 | paths: 44 | - path: /{*path} 45 | service: 46 | name: api-service 47 | ``` 48 | 49 | Use host matching to separate traffic by domain or subdomain. Wildcard `*` catches any host that did not match previously defined routes. 50 | 51 | ### Header-based routing 52 | 53 | Enable multi-tenant or environment-specific configurations by inspecting a request header. Set a `header_selector` at the top of your config, then bind each header value to a route. 54 | 55 | ```yaml 56 | header_selector: x-nylon-environment 57 | 58 | routes: 59 | - route: 60 | type: header 61 | value: staging 62 | name: staging-app 63 | paths: 64 | - path: /{*path} 65 | service: 66 | name: staging-backend 67 | ``` 68 | 69 | ### Method filtering 70 | 71 | Restrict a path to specific HTTP methods. Nylon accepts `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, and `TRACE`. 72 | 73 | ```yaml 74 | paths: 75 | - path: /api/users 76 | methods: 77 | - GET 78 | - POST 79 | service: 80 | name: user-service 81 | ``` 82 | 83 | ## Path Patterns 84 | 85 | | Pattern | Matches | Notes | 86 | | ------------------- | ------------------------------------ | ------------------------------------- | 87 | | `/status` | Exact `/status` | Fastest match | 88 | | `/users/{id}` | Any single segment (`/users/42`) | Captured as `params["id"]` | 89 | | `/assets/{*path}` | All trailing segments | Catch-all; lowest priority | 90 | | `/{*path}` | Everything | Use as a final fallback | 91 | 92 | Extracted parameters are available inside plugins: 93 | 94 | ```go 95 | phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) { 96 | if userID, ok := ctx.Request().Params()["id"]; ok { 97 | ctx.Logger().Info("routing user", "id", userID) 98 | } 99 | ctx.Next() 100 | }) 101 | ``` 102 | 103 | ### Multiple paths per route 104 | 105 | Organize related paths under the same route and share middleware when needed. 106 | 107 | ```yaml 108 | routes: 109 | - route: 110 | type: host 111 | value: app.example.com 112 | paths: 113 | - path: /api/{*path} 114 | service: 115 | name: api 116 | middleware: 117 | - plugin: auth 118 | entry: check 119 | - path: /static/{*path} 120 | service: 121 | name: cdn 122 | - path: 123 | - / 124 | - /{*path} 125 | service: 126 | name: web 127 | ``` 128 | 129 | ## Path Rewrites 130 | 131 | Rewrites adjust the upstream request path without changing the path matched by the client. 132 | 133 | ```yaml 134 | paths: 135 | - path: /old-api/{*path} 136 | service: 137 | name: new-api 138 | rewrite: /v2 139 | 140 | - path: /api/v1/{*path} 141 | service: 142 | name: api-v1 143 | rewrite: / 144 | ``` 145 | 146 | - When the route matches `/old-api/users`, Nylon proxies to `/v2/users`. 147 | - Use `/` to strip a prefix entirely. 148 | 149 | ## How Matching Order Works 150 | 151 | Nylon uses [`matchit` v0.8](https://docs.rs/crate/matchit/latest) to score routes: 152 | 153 | 1. Exact segments (`/health`) take priority. 154 | 2. Named parameters (`/{user}`) run next. 155 | 3. Catch-all parameters (`/{*path}`) match last. 156 | 157 | ```yaml 158 | paths: 159 | - path: /api/health # 1 — exact 160 | - path: /api/{resource} # 2 — named parameter 161 | - path: /{*path} # 3 — catch-all fallback 162 | service: 163 | name: fallback 164 | ``` 165 | 166 | Order still matters when two paths have the same precedence—define the most specific entries first. 167 | 168 | ## Dynamic Routing & Segmentation 169 | 170 | Combine host, header, and method rules to isolate workloads or tenants. 171 | 172 | ```yaml 173 | header_selector: x-nylon-proxy 174 | 175 | routes: 176 | - route: 177 | type: header 178 | value: tenant-a 179 | name: tenant-a 180 | paths: 181 | - path: /admin/{*path} 182 | service: 183 | name: admin 184 | methods: 185 | - GET 186 | - POST 187 | middleware: 188 | - plugin: auth 189 | entry: admin 190 | - path: /{*path} 191 | service: 192 | name: app 193 | ``` 194 | 195 | Requests with `x-nylon-proxy: tenant-a` use the above layout, while other values can map to different services or environments. 196 | 197 | ## End-to-end Example 198 | 199 | ```yaml 200 | header_selector: x-nylon-proxy 201 | 202 | routes: 203 | - route: 204 | type: host 205 | value: api.example.com 206 | name: api 207 | tls: 208 | enabled: true 209 | middleware: 210 | - group: observability 211 | paths: 212 | - path: /public/{*path} 213 | service: 214 | name: public-api 215 | - path: /v1/{*path} 216 | service: 217 | name: api-v1 218 | middleware: 219 | - plugin: auth 220 | entry: jwt 221 | - path: /admin/{*path} 222 | methods: 223 | - GET 224 | - POST 225 | service: 226 | name: admin-api 227 | 228 | - route: 229 | type: host 230 | value: static.example.com 231 | name: static 232 | paths: 233 | - path: 234 | - / 235 | - /{*path} 236 | service: 237 | name: cdn 238 | 239 | - route: 240 | type: host 241 | value: "*" 242 | name: default 243 | paths: 244 | - path: 245 | - / 246 | - /{*path} 247 | service: 248 | name: default-backend 249 | ``` 250 | 251 | ## Best Practices 252 | 253 | 1. **Lead with specificity**: Put the narrowest path first and reserve catch-all entries for the bottom. 254 | 2. **Group shared behavior**: Use middleware groups to apply authentication, rate limiting, or logging policies consistently. 255 | 3. **Segment by domain**: Split public, admin, API, and static traffic into separate routes for clarity. 256 | 4. **Prefer parameters over wildcards**: Named segments make logs and plugins easier to reason about. 257 | 5. **Document rewrites**: Include comments or naming conventions so teams understand why a rewrite exists. 258 | 259 | ## Next Steps 260 | 261 | - [Configuration](/core/configuration) for every available field. 262 | - [Middleware](/core/middleware) to attach logic to routes. 263 | - [Examples](/examples/basic-proxy) for complete proxy configurations. 264 | -------------------------------------------------------------------------------- /crates/nylon-plugin/src/stream.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::not_unsafe_ptr_arg_deref)] 2 | 3 | use async_trait::async_trait; 4 | use nylon_error::NylonError; 5 | use nylon_types::plugins::{FfiBuffer, FfiPlugin, PluginPhase, SessionStream}; 6 | use nylon_types::websocket::WebSocketMessage; 7 | use once_cell::sync::Lazy; 8 | use std::{ 9 | collections::HashMap, 10 | sync::{ 11 | Arc, RwLock, 12 | atomic::{AtomicU32, Ordering}, 13 | }, 14 | }; 15 | use tokio::sync::Mutex; 16 | use tokio::sync::mpsc::UnboundedReceiver as UnboundedWsReceiver; 17 | use tokio::sync::mpsc::{self, UnboundedReceiver}; 18 | use tracing::{debug, trace}; 19 | 20 | // Active sessions 21 | type SessionSender = mpsc::UnboundedSender<(u32, Vec)>; 22 | 23 | static ACTIVE_SESSIONS: Lazy>> = 24 | Lazy::new(|| RwLock::new(HashMap::new())); 25 | static NEXT_SESSION_ID: AtomicU32 = AtomicU32::new(1); 26 | // static SESSION_RX: Lazy)>>>>>>> = 27 | // Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); 28 | static SESSION_RX: Lazy)>>>>>> = 29 | Lazy::new(|| Mutex::new(HashMap::new())); 30 | 31 | // WS message receivers per session for cluster/local adapter dispatch 32 | static SESSION_WS_RX: Lazy>>>>> = 33 | Lazy::new(|| Mutex::new(HashMap::new())); 34 | 35 | #[unsafe(no_mangle)] 36 | pub extern "C" fn handle_ffi_event(data: *const FfiBuffer) { 37 | let ffi = unsafe { &*data }; 38 | let session_id = ffi.sid; 39 | let method = ffi.method; 40 | // println!("handle_ffi_event: session_id={}, method={}, phase={}", session_id, method, ffi.phase); 41 | // let phase = ffi.phase; 42 | let len = ffi.len as usize; 43 | let ptr = ffi.ptr; 44 | trace!( 45 | "handle_ffi_event: session_id={}, method={}", 46 | session_id, method 47 | ); 48 | // Clone sender first to minimize time under the read lock 49 | let sender_opt = ACTIVE_SESSIONS 50 | .read() 51 | .ok() 52 | .and_then(|sessions| sessions.get(&session_id).cloned()); 53 | 54 | if let Some(sender) = sender_opt { 55 | if ptr.is_null() { 56 | trace!("handle_ffi_event: null payload"); 57 | // println!("handle_ffi_event: null payload"); 58 | let _ = sender.send((method, Vec::new())); 59 | return; 60 | } 61 | 62 | unsafe { 63 | // Fast copy from raw pointer without creating uninitialized values 64 | let slice = std::slice::from_raw_parts(ptr, len); 65 | let buf = slice.to_vec(); 66 | // println!("handle_ffi_event: session_id={}, method={}, phase={}", session_id, method, ffi.phase); 67 | // println!("handle_ffi_event: buf={:?}", buf); 68 | // println!("handle_ffi_event: buf len={}", buf.len()); 69 | // println!("handle_ffi_event: buf as string={}", String::from_utf8_lossy(&buf)); 70 | if let Err(_e) = sender.send((method, buf)) { 71 | debug!("send error: {:?}", session_id); 72 | } 73 | } 74 | } else { 75 | trace!("handle_ffi_event: no active session for sid={}", session_id); 76 | } 77 | } 78 | 79 | // === SessionStream trait === 80 | #[async_trait] 81 | pub trait PluginSessionStream { 82 | fn new(plugin: Arc, session_id: u32) -> Self; 83 | async fn open(&self, entry: &str) -> Result; 84 | async fn event_stream( 85 | &self, 86 | phase: PluginPhase, 87 | method: u32, 88 | data: &[u8], 89 | ) -> Result<(), NylonError>; 90 | async fn close(&self) -> Result<(), NylonError>; 91 | } 92 | 93 | #[async_trait] 94 | impl PluginSessionStream for SessionStream { 95 | fn new(plugin: Arc, session_id: u32) -> Self { 96 | if session_id == 0 { 97 | let session_id = NEXT_SESSION_ID.fetch_add(1, Ordering::Relaxed); 98 | Self { plugin, session_id } 99 | } else { 100 | Self { plugin, session_id } 101 | } 102 | } 103 | 104 | async fn open(&self, entry: &str) -> Result { 105 | let (tx, rx) = mpsc::unbounded_channel(); 106 | 107 | { 108 | let mut sessions = ACTIVE_SESSIONS.write().map_err(|e| { 109 | NylonError::ConfigError(format!("Failed to lock ACTIVE_SESSIONS: {:?}", e)) 110 | })?; 111 | sessions.insert(self.session_id, tx); 112 | } 113 | 114 | unsafe { 115 | let ok = (*self.plugin.register_session)( 116 | self.session_id, 117 | entry.as_ptr(), 118 | entry.len() as u32, 119 | handle_ffi_event, 120 | ); 121 | if !ok { 122 | if let Ok(mut sessions) = ACTIVE_SESSIONS.write() { 123 | sessions.remove(&self.session_id); 124 | } 125 | return Err(NylonError::ConfigError( 126 | "Failed to register session".to_string(), 127 | )); 128 | } 129 | } 130 | { 131 | // let mut sessions = SESSION_RX.lock().await; 132 | // sessions.insert(self.session_id, Arc::new(Mutex::new(rx))); 133 | { 134 | let mut sessions = SESSION_RX.lock().await; 135 | sessions.insert(self.session_id, Arc::new(Mutex::new(rx))); 136 | } 137 | } 138 | Ok(self.session_id) 139 | } 140 | 141 | async fn event_stream( 142 | &self, 143 | phase: PluginPhase, 144 | method: u32, 145 | data: &[u8], 146 | ) -> Result<(), NylonError> { 147 | let ffi_buffer = &FfiBuffer { 148 | sid: self.session_id, 149 | phase: phase.to_u8(), 150 | method, 151 | ptr: data.as_ptr(), 152 | len: data.len() as u64, 153 | }; 154 | unsafe { 155 | (*self.plugin.event_stream)(ffi_buffer); 156 | } 157 | Ok(()) 158 | } 159 | 160 | async fn close(&self) -> Result<(), NylonError> { 161 | let _ = close_session(self.plugin.clone(), self.session_id).await?; 162 | Ok(()) 163 | } 164 | } 165 | 166 | pub async fn close_session(plugin: Arc, session_id: u32) -> Result<(), NylonError> { 167 | unsafe { 168 | (*plugin.close_session)(session_id); 169 | } 170 | if let Ok(mut sessions) = ACTIVE_SESSIONS.write() { 171 | sessions.remove(&session_id); 172 | } 173 | Ok(()) 174 | } 175 | 176 | pub fn get_rx( 177 | session_id: u32, 178 | ) -> Result)>>>, NylonError> { 179 | let sessions = SESSION_RX.try_lock(); 180 | // sessions 181 | // .get(&session_id) 182 | // .cloned() 183 | // .ok_or_else(|| NylonError::ConfigError(format!("Session {} not found", session_id))) 184 | // .map(|arc| arc.clone()) 185 | match sessions { 186 | Ok(sessions) => sessions 187 | .get(&session_id) 188 | .cloned() 189 | .ok_or_else(|| NylonError::ConfigError(format!("Session {} not found", session_id))), 190 | Err(_) => Err(NylonError::ConfigError( 191 | "Failed to lock SESSION_RX".to_string(), 192 | )), 193 | } 194 | } 195 | 196 | pub async fn set_ws_rx( 197 | session_id: u32, 198 | rx: UnboundedWsReceiver, 199 | ) -> Result<(), NylonError> { 200 | let mut sessions = SESSION_WS_RX.lock().await; 201 | sessions.insert(session_id, Arc::new(Mutex::new(rx))); 202 | Ok(()) 203 | } 204 | 205 | pub fn get_ws_rx( 206 | session_id: u32, 207 | ) -> Result>>, NylonError> { 208 | let sessions = SESSION_WS_RX.try_lock(); 209 | match sessions { 210 | Ok(sessions) => sessions 211 | .get(&session_id) 212 | .cloned() 213 | .ok_or_else(|| NylonError::ConfigError(format!("WS Session {} not found", session_id))), 214 | Err(_) => Err(NylonError::ConfigError( 215 | "Failed to lock SESSION_WS_RX".to_string(), 216 | )), 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /docs/public/install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Colors for output 6 | RED='\033[0;31m' 7 | GREEN='\033[0;32m' 8 | YELLOW='\033[1;33m' 9 | BLUE='\033[0;34m' 10 | NC='\033[0m' # No Color 11 | 12 | # Configuration 13 | GITHUB_REPO="AssetsArt/nylon" 14 | BINARY_NAME="nylon" 15 | INSTALL_DIR="/usr/local/bin" 16 | 17 | # Print colored message 18 | print_message() { 19 | local color=$1 20 | shift 21 | echo -e "${color}$@${NC}" 22 | } 23 | 24 | print_info() { 25 | print_message "$BLUE" "ℹ️ $@" 26 | } 27 | 28 | print_success() { 29 | print_message "$GREEN" "✅ $@" 30 | } 31 | 32 | print_warning() { 33 | print_message "$YELLOW" "⚠️ $@" 34 | } 35 | 36 | print_error() { 37 | print_message "$RED" "❌ $@" 38 | } 39 | 40 | # Check if command exists 41 | command_exists() { 42 | command -v "$1" >/dev/null 2>&1 43 | } 44 | 45 | # Detect OS 46 | detect_os() { 47 | case "$(uname -s)" in 48 | Linux*) 49 | echo "linux" 50 | ;; 51 | Darwin*) 52 | echo "darwin" 53 | ;; 54 | *) 55 | echo "unknown" 56 | ;; 57 | esac 58 | } 59 | 60 | # Detect architecture 61 | detect_arch() { 62 | case "$(uname -m)" in 63 | x86_64|amd64) 64 | echo "x86_64" 65 | ;; 66 | aarch64|arm64) 67 | echo "aarch64" 68 | ;; 69 | *) 70 | echo "unknown" 71 | ;; 72 | esac 73 | } 74 | 75 | # Detect libc variant (gnu or musl) 76 | detect_libc() { 77 | if command_exists ldd; then 78 | if ldd --version 2>&1 | grep -q musl; then 79 | echo "musl" 80 | else 81 | echo "gnu" 82 | fi 83 | else 84 | # Default to musl if ldd not found (safer for static linking) 85 | echo "musl" 86 | fi 87 | } 88 | 89 | # Get latest version from GitHub 90 | get_latest_version() { 91 | if command_exists curl; then 92 | curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | \ 93 | grep '"tag_name":' | \ 94 | sed -E 's/.*"v([^"]+)".*/\1/' 95 | else 96 | print_error "curl is required but not installed" 97 | exit 1 98 | fi 99 | } 100 | 101 | # Download file 102 | download_file() { 103 | local url=$1 104 | local output=$2 105 | 106 | if command_exists curl; then 107 | curl -fsSL -o "$output" "$url" 108 | elif command_exists wget; then 109 | wget -q -O "$output" "$url" 110 | else 111 | print_error "curl or wget is required but neither is installed" 112 | exit 1 113 | fi 114 | } 115 | 116 | # Verify checksum 117 | verify_checksum() { 118 | local file=$1 119 | local checksum_file=$2 120 | local binary_name=$3 121 | 122 | if command_exists shasum; then 123 | local expected_checksum=$(grep "$binary_name" "$checksum_file" | awk '{print $1}') 124 | local actual_checksum=$(shasum -a 256 "$file" | awk '{print $1}') 125 | 126 | if [ "$expected_checksum" = "$actual_checksum" ]; then 127 | return 0 128 | else 129 | return 1 130 | fi 131 | else 132 | print_warning "shasum not found, skipping checksum verification" 133 | return 0 134 | fi 135 | } 136 | 137 | # Main installation 138 | main() { 139 | print_info "🚀 Nylon Proxy Installer" 140 | echo "" 141 | 142 | # Check required commands 143 | if ! command_exists curl && ! command_exists wget; then 144 | print_error "curl or wget is required for installation" 145 | exit 1 146 | fi 147 | 148 | # Detect system 149 | print_info "Detecting system information..." 150 | OS=$(detect_os) 151 | ARCH=$(detect_arch) 152 | 153 | if [ "$OS" = "unknown" ]; then 154 | print_error "Unsupported operating system: $(uname -s)" 155 | exit 1 156 | fi 157 | 158 | if [ "$ARCH" = "unknown" ]; then 159 | print_error "Unsupported architecture: $(uname -m)" 160 | exit 1 161 | fi 162 | 163 | print_success "OS: $OS, Architecture: $ARCH" 164 | 165 | # Check if macOS 166 | if [ "$OS" = "darwin" ]; then 167 | print_error "macOS binaries are not available yet." 168 | print_info "Please build from source:" 169 | echo "" 170 | echo " git clone https://github.com/${GITHUB_REPO}.git" 171 | echo " cd nylon" 172 | echo " cargo build --release" 173 | echo " sudo cp target/release/nylon /usr/local/bin/" 174 | echo "" 175 | exit 1 176 | fi 177 | 178 | # Detect libc for Linux 179 | LIBC=$(detect_libc) 180 | print_success "Libc: $LIBC" 181 | 182 | # Construct binary name 183 | BINARY_VARIANT="${BINARY_NAME}-${ARCH}-linux-${LIBC}" 184 | print_info "Binary variant: $BINARY_VARIANT" 185 | 186 | # Get latest version 187 | print_info "Fetching latest version..." 188 | VERSION=$(get_latest_version) 189 | 190 | if [ -z "$VERSION" ]; then 191 | print_error "Failed to fetch latest version" 192 | exit 1 193 | fi 194 | 195 | print_success "Latest version: v${VERSION}" 196 | 197 | # Construct download URLs 198 | BASE_URL="https://github.com/${GITHUB_REPO}/releases/download/v${VERSION}" 199 | BINARY_URL="${BASE_URL}/${BINARY_VARIANT}" 200 | CHECKSUM_URL="${BASE_URL}/linux-checksums.txt" 201 | 202 | # Create temp directory 203 | TMP_DIR=$(mktemp -d) 204 | trap "rm -rf $TMP_DIR" EXIT 205 | 206 | print_info "Downloading ${BINARY_VARIANT}..." 207 | if ! download_file "$BINARY_URL" "$TMP_DIR/$BINARY_VARIANT"; then 208 | print_error "Failed to download binary" 209 | exit 1 210 | fi 211 | print_success "Downloaded binary" 212 | 213 | print_info "Downloading checksums..." 214 | if ! download_file "$CHECKSUM_URL" "$TMP_DIR/checksums.txt"; then 215 | print_warning "Failed to download checksums, skipping verification" 216 | else 217 | print_info "Verifying checksum..." 218 | if verify_checksum "$TMP_DIR/$BINARY_VARIANT" "$TMP_DIR/checksums.txt" "$BINARY_VARIANT"; then 219 | print_success "Checksum verified" 220 | else 221 | print_error "Checksum verification failed" 222 | exit 1 223 | fi 224 | fi 225 | 226 | # Determine install directory 227 | if [ -w "$INSTALL_DIR" ]; then 228 | FINAL_INSTALL_DIR="$INSTALL_DIR" 229 | elif [ "$EUID" -eq 0 ] || [ "$(id -u)" -eq 0 ]; then 230 | FINAL_INSTALL_DIR="$INSTALL_DIR" 231 | else 232 | FINAL_INSTALL_DIR="$HOME/.local/bin" 233 | print_warning "No write permission to $INSTALL_DIR, installing to $FINAL_INSTALL_DIR" 234 | mkdir -p "$FINAL_INSTALL_DIR" 235 | fi 236 | 237 | # Install binary 238 | print_info "Installing to ${FINAL_INSTALL_DIR}/${BINARY_NAME}..." 239 | 240 | if [ -w "$FINAL_INSTALL_DIR" ]; then 241 | mv "$TMP_DIR/$BINARY_VARIANT" "$FINAL_INSTALL_DIR/$BINARY_NAME" 242 | chmod +x "$FINAL_INSTALL_DIR/$BINARY_NAME" 243 | else 244 | sudo mv "$TMP_DIR/$BINARY_VARIANT" "$FINAL_INSTALL_DIR/$BINARY_NAME" 245 | sudo chmod +x "$FINAL_INSTALL_DIR/$BINARY_NAME" 246 | fi 247 | 248 | print_success "Installed successfully!" 249 | 250 | # Check if in PATH 251 | if ! echo "$PATH" | grep -q "$FINAL_INSTALL_DIR"; then 252 | print_warning "$FINAL_INSTALL_DIR is not in your PATH" 253 | print_info "Add this to your shell profile:" 254 | echo "" 255 | echo " export PATH=\"\$PATH:$FINAL_INSTALL_DIR\"" 256 | echo "" 257 | fi 258 | 259 | # Verify installation 260 | if command_exists "$BINARY_NAME"; then 261 | echo "" 262 | print_success "Installation complete! 🎉" 263 | echo "" 264 | print_info "Installed version:" 265 | "$BINARY_NAME" --version 2>/dev/null || echo " nylon v${VERSION}" 266 | echo "" 267 | print_info "Quick start:" 268 | echo " nylon run -c config.yaml" 269 | echo "" 270 | print_info "Documentation: https://nylon.sh/" 271 | else 272 | print_warning "Installation complete, but 'nylon' command not found in PATH" 273 | print_info "Try running: $FINAL_INSTALL_DIR/$BINARY_NAME --version" 274 | fi 275 | } 276 | 277 | main "$@" 278 | 279 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Colors for output 6 | RED='\033[0;31m' 7 | GREEN='\033[0;32m' 8 | YELLOW='\033[1;33m' 9 | BLUE='\033[0;34m' 10 | NC='\033[0m' # No Color 11 | 12 | # Configuration 13 | GITHUB_REPO="AssetsArt/nylon" 14 | BINARY_NAME="nylon" 15 | INSTALL_DIR="/usr/local/bin" 16 | 17 | # Print colored message 18 | print_message() { 19 | local color=$1 20 | shift 21 | echo -e "${color}$@${NC}" 22 | } 23 | 24 | print_info() { 25 | print_message "$BLUE" "ℹ️ $@" 26 | } 27 | 28 | print_success() { 29 | print_message "$GREEN" "✅ $@" 30 | } 31 | 32 | print_warning() { 33 | print_message "$YELLOW" "⚠️ $@" 34 | } 35 | 36 | print_error() { 37 | print_message "$RED" "❌ $@" 38 | } 39 | 40 | # Check if command exists 41 | command_exists() { 42 | command -v "$1" >/dev/null 2>&1 43 | } 44 | 45 | # Detect OS 46 | detect_os() { 47 | case "$(uname -s)" in 48 | Linux*) 49 | echo "linux" 50 | ;; 51 | Darwin*) 52 | echo "darwin" 53 | ;; 54 | *) 55 | echo "unknown" 56 | ;; 57 | esac 58 | } 59 | 60 | # Detect architecture 61 | detect_arch() { 62 | case "$(uname -m)" in 63 | x86_64|amd64) 64 | echo "x86_64" 65 | ;; 66 | aarch64|arm64) 67 | echo "aarch64" 68 | ;; 69 | *) 70 | echo "unknown" 71 | ;; 72 | esac 73 | } 74 | 75 | # Detect libc variant (gnu or musl) 76 | detect_libc() { 77 | if command_exists ldd; then 78 | if ldd --version 2>&1 | grep -q musl; then 79 | echo "musl" 80 | else 81 | echo "gnu" 82 | fi 83 | else 84 | # Default to musl if ldd not found (safer for static linking) 85 | echo "musl" 86 | fi 87 | } 88 | 89 | # Get latest version from GitHub 90 | get_latest_version() { 91 | if command_exists curl; then 92 | curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | \ 93 | grep '"tag_name":' | \ 94 | sed -E 's/.*"v([^"]+)".*/\1/' 95 | else 96 | print_error "curl is required but not installed" 97 | exit 1 98 | fi 99 | } 100 | 101 | # Download file 102 | download_file() { 103 | local url=$1 104 | local output=$2 105 | 106 | if command_exists curl; then 107 | curl -fsSL -o "$output" "$url" 108 | elif command_exists wget; then 109 | wget -q -O "$output" "$url" 110 | else 111 | print_error "curl or wget is required but neither is installed" 112 | exit 1 113 | fi 114 | } 115 | 116 | # Verify checksum 117 | verify_checksum() { 118 | local file=$1 119 | local checksum_file=$2 120 | local binary_name=$3 121 | 122 | if command_exists shasum; then 123 | local expected_checksum=$(grep "$binary_name" "$checksum_file" | awk '{print $1}') 124 | local actual_checksum=$(shasum -a 256 "$file" | awk '{print $1}') 125 | 126 | if [ "$expected_checksum" = "$actual_checksum" ]; then 127 | return 0 128 | else 129 | return 1 130 | fi 131 | else 132 | print_warning "shasum not found, skipping checksum verification" 133 | return 0 134 | fi 135 | } 136 | 137 | # Main installation 138 | main() { 139 | print_info "🚀 Nylon Proxy Installer" 140 | echo "" 141 | 142 | # Check required commands 143 | if ! command_exists curl && ! command_exists wget; then 144 | print_error "curl or wget is required for installation" 145 | exit 1 146 | fi 147 | 148 | # Detect system 149 | print_info "Detecting system information..." 150 | OS=$(detect_os) 151 | ARCH=$(detect_arch) 152 | 153 | if [ "$OS" = "unknown" ]; then 154 | print_error "Unsupported operating system: $(uname -s)" 155 | exit 1 156 | fi 157 | 158 | if [ "$ARCH" = "unknown" ]; then 159 | print_error "Unsupported architecture: $(uname -m)" 160 | exit 1 161 | fi 162 | 163 | print_success "OS: $OS, Architecture: $ARCH" 164 | 165 | # Check if macOS 166 | if [ "$OS" = "darwin" ]; then 167 | print_error "macOS binaries are not available yet." 168 | print_info "Please build from source:" 169 | echo "" 170 | echo " git clone https://github.com/${GITHUB_REPO}.git" 171 | echo " cd nylon" 172 | echo " cargo build --release" 173 | echo " sudo cp target/release/nylon /usr/local/bin/" 174 | echo "" 175 | exit 1 176 | fi 177 | 178 | # Detect libc for Linux 179 | LIBC=$(detect_libc) 180 | print_success "Libc: $LIBC" 181 | 182 | # Construct binary name 183 | BINARY_VARIANT="${BINARY_NAME}-${ARCH}-linux-${LIBC}" 184 | print_info "Binary variant: $BINARY_VARIANT" 185 | 186 | # Get latest version 187 | print_info "Fetching latest version..." 188 | VERSION=$(get_latest_version) 189 | 190 | if [ -z "$VERSION" ]; then 191 | print_error "Failed to fetch latest version" 192 | exit 1 193 | fi 194 | 195 | print_success "Latest version: v${VERSION}" 196 | 197 | # Construct download URLs 198 | BASE_URL="https://github.com/${GITHUB_REPO}/releases/download/v${VERSION}" 199 | BINARY_URL="${BASE_URL}/${BINARY_VARIANT}" 200 | CHECKSUM_URL="${BASE_URL}/linux-checksums.txt" 201 | 202 | # Create temp directory 203 | TMP_DIR=$(mktemp -d) 204 | trap "rm -rf $TMP_DIR" EXIT 205 | 206 | print_info "Downloading ${BINARY_VARIANT}..." 207 | if ! download_file "$BINARY_URL" "$TMP_DIR/$BINARY_VARIANT"; then 208 | print_error "Failed to download binary" 209 | exit 1 210 | fi 211 | print_success "Downloaded binary" 212 | 213 | print_info "Downloading checksums..." 214 | if ! download_file "$CHECKSUM_URL" "$TMP_DIR/checksums.txt"; then 215 | print_warning "Failed to download checksums, skipping verification" 216 | else 217 | print_info "Verifying checksum..." 218 | if verify_checksum "$TMP_DIR/$BINARY_VARIANT" "$TMP_DIR/checksums.txt" "$BINARY_VARIANT"; then 219 | print_success "Checksum verified" 220 | else 221 | print_error "Checksum verification failed" 222 | exit 1 223 | fi 224 | fi 225 | 226 | # Determine install directory 227 | if [ -w "$INSTALL_DIR" ]; then 228 | FINAL_INSTALL_DIR="$INSTALL_DIR" 229 | elif [ "$EUID" -eq 0 ] || [ "$(id -u)" -eq 0 ]; then 230 | FINAL_INSTALL_DIR="$INSTALL_DIR" 231 | else 232 | FINAL_INSTALL_DIR="$HOME/.local/bin" 233 | print_warning "No write permission to $INSTALL_DIR, installing to $FINAL_INSTALL_DIR" 234 | mkdir -p "$FINAL_INSTALL_DIR" 235 | fi 236 | 237 | # Install binary 238 | print_info "Installing to ${FINAL_INSTALL_DIR}/${BINARY_NAME}..." 239 | 240 | if [ -w "$FINAL_INSTALL_DIR" ]; then 241 | mv "$TMP_DIR/$BINARY_VARIANT" "$FINAL_INSTALL_DIR/$BINARY_NAME" 242 | chmod +x "$FINAL_INSTALL_DIR/$BINARY_NAME" 243 | else 244 | sudo mv "$TMP_DIR/$BINARY_VARIANT" "$FINAL_INSTALL_DIR/$BINARY_NAME" 245 | sudo chmod +x "$FINAL_INSTALL_DIR/$BINARY_NAME" 246 | fi 247 | 248 | print_success "Installed successfully!" 249 | 250 | # Check if in PATH 251 | if ! echo "$PATH" | grep -q "$FINAL_INSTALL_DIR"; then 252 | print_warning "$FINAL_INSTALL_DIR is not in your PATH" 253 | print_info "Add this to your shell profile:" 254 | echo "" 255 | echo " export PATH=\"\$PATH:$FINAL_INSTALL_DIR\"" 256 | echo "" 257 | fi 258 | 259 | # Verify installation 260 | if command_exists "$BINARY_NAME"; then 261 | echo "" 262 | print_success "Installation complete! 🎉" 263 | echo "" 264 | print_info "Installed version:" 265 | "$BINARY_NAME" --version 2>/dev/null || echo " nylon v${VERSION}" 266 | echo "" 267 | print_info "Quick start:" 268 | echo " nylon run -c config.yaml" 269 | echo "" 270 | print_info "Documentation: https://nylon.sh/" 271 | else 272 | print_warning "Installation complete, but 'nylon' command not found in PATH" 273 | print_info "Try running: $FINAL_INSTALL_DIR/$BINARY_NAME --version" 274 | fi 275 | } 276 | 277 | main "$@" 278 | 279 | --------------------------------------------------------------------------------