outpunch-ruby 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/ext/outpunch/Cargo.toml +18 -0
- data/ext/outpunch/extconf.rb +4 -0
- data/ext/outpunch/src/gvl.rs +67 -0
- data/ext/outpunch/src/lib.rs +168 -0
- data/ext/outpunch/src/lib_tests.rs +36 -0
- data/ext/outpunch/src/server.rs +241 -0
- data/lib/outpunch-ruby.rb +3 -0
- data/lib/outpunch.rb +3 -0
- metadata +93 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3d1ce909def454c1c033202a6e4fcd56da57df62aa9136ff8b8ca83e0a994d35
|
|
4
|
+
data.tar.gz: 93b88087d3fc832fc2ee33e9f209464f36700245961127f02e47ee64669c7236
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 623853f37971fe05a61db5b342ff8ac2ea017b62fc0b4b0010c195951138b80f357782c86812d6c0cd4ac4c38c33f121f0dd4f1c4f52af4a88d84604afc820cf
|
|
7
|
+
data.tar.gz: 12886aaeb116eba5419f7090203e820cc392cc51e64936651e9a16496f15d3a526e02fa9ce10485d23b877ae2f9b7f2fd059aa24492515062768f988e35b2fa8
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "outpunch"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
|
|
6
|
+
[lib]
|
|
7
|
+
name = "outpunch"
|
|
8
|
+
crate-type = ["cdylib"]
|
|
9
|
+
|
|
10
|
+
[dependencies]
|
|
11
|
+
outpunch-client = { version = "0.5.0" }
|
|
12
|
+
magnus = { version = "0.8" }
|
|
13
|
+
rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
|
|
14
|
+
tokio = { version = "1.50.0", features = ["full"] }
|
|
15
|
+
outpunch_core = { version = "0.5.0", package = "outpunch" }
|
|
16
|
+
|
|
17
|
+
[build-dependencies]
|
|
18
|
+
rb-sys-build = "0.9"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
use std::ffi::c_void;
|
|
2
|
+
use std::panic::{self, AssertUnwindSafe};
|
|
3
|
+
use std::sync::Arc;
|
|
4
|
+
|
|
5
|
+
use magnus::{Error, Ruby};
|
|
6
|
+
use tokio::sync::Notify;
|
|
7
|
+
|
|
8
|
+
struct Call<F, R> {
|
|
9
|
+
func: Option<F>,
|
|
10
|
+
result: Option<std::thread::Result<R>>,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
unsafe extern "C" fn call<F: FnOnce() -> R, R>(data: *mut c_void) -> *mut c_void {
|
|
14
|
+
let call = unsafe { &mut *(data as *mut Call<F, R>) };
|
|
15
|
+
if let Some(func) = call.func.take() {
|
|
16
|
+
call.result = Some(panic::catch_unwind(AssertUnwindSafe(func)));
|
|
17
|
+
}
|
|
18
|
+
std::ptr::null_mut()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fn trampoline<F: FnOnce() -> R, R>(
|
|
22
|
+
_: &Call<F, R>,
|
|
23
|
+
) -> unsafe extern "C" fn(*mut c_void) -> *mut c_void {
|
|
24
|
+
call::<F, R>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
unsafe extern "C" fn unblock(data: *mut c_void) {
|
|
28
|
+
let cancel = unsafe { &*(data as *const Notify) };
|
|
29
|
+
cancel.notify_one();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Run `func` with the GVL released. `func` receives a `Notify` that fires when Ruby
|
|
33
|
+
/// interrupts the thread (Thread#kill, Thread#raise, Timeout); it returns `None` when
|
|
34
|
+
/// it stopped early because of that.
|
|
35
|
+
///
|
|
36
|
+
/// Returns `Ok(None)` when the call was cancelled and Ruby had no exception pending.
|
|
37
|
+
pub fn without_gvl<F, R>(ruby: &Ruby, func: F) -> Result<Option<R>, Error>
|
|
38
|
+
where
|
|
39
|
+
F: FnOnce(Arc<Notify>) -> Option<R>,
|
|
40
|
+
{
|
|
41
|
+
let cancel = Arc::new(Notify::new());
|
|
42
|
+
let mut state = Call {
|
|
43
|
+
func: Some({
|
|
44
|
+
let cancel = cancel.clone();
|
|
45
|
+
move || func(cancel)
|
|
46
|
+
}),
|
|
47
|
+
result: None,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
unsafe {
|
|
51
|
+
rb_sys::rb_thread_call_without_gvl2(
|
|
52
|
+
Some(trampoline(&state)),
|
|
53
|
+
&mut state as *mut _ as *mut c_void,
|
|
54
|
+
Some(unblock),
|
|
55
|
+
Arc::as_ptr(&cancel) as *mut c_void,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
match state.result {
|
|
60
|
+
Some(Ok(Some(value))) => Ok(Some(value)),
|
|
61
|
+
Some(Ok(None)) | None => {
|
|
62
|
+
ruby.thread_check_ints()?;
|
|
63
|
+
Ok(None)
|
|
64
|
+
}
|
|
65
|
+
Some(Err(payload)) => panic::resume_unwind(payload),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
use std::sync::Mutex;
|
|
2
|
+
use std::time::Duration;
|
|
3
|
+
|
|
4
|
+
use magnus::{Error, Ruby, function, method, prelude::*, wrap};
|
|
5
|
+
|
|
6
|
+
mod gvl;
|
|
7
|
+
mod server;
|
|
8
|
+
|
|
9
|
+
struct ClientConfigInner {
|
|
10
|
+
server_url: String,
|
|
11
|
+
secret: String,
|
|
12
|
+
service: String,
|
|
13
|
+
forward_to: String,
|
|
14
|
+
reconnect_delay: f64,
|
|
15
|
+
request_timeout: f64,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[wrap(class = "Outpunch::ClientConfig", free_immediately, size)]
|
|
19
|
+
pub struct ClientConfig(Mutex<ClientConfigInner>);
|
|
20
|
+
|
|
21
|
+
impl ClientConfig {
|
|
22
|
+
fn new(
|
|
23
|
+
server_url: String,
|
|
24
|
+
secret: String,
|
|
25
|
+
service: String,
|
|
26
|
+
forward_to: Option<String>,
|
|
27
|
+
reconnect_delay: Option<f64>,
|
|
28
|
+
request_timeout: Option<f64>,
|
|
29
|
+
) -> Self {
|
|
30
|
+
Self(Mutex::new(ClientConfigInner {
|
|
31
|
+
server_url,
|
|
32
|
+
secret,
|
|
33
|
+
service,
|
|
34
|
+
forward_to: forward_to.unwrap_or_else(|| "http://localhost:8080".into()),
|
|
35
|
+
reconnect_delay: reconnect_delay.unwrap_or(5.0),
|
|
36
|
+
request_timeout: request_timeout.unwrap_or(25.0),
|
|
37
|
+
}))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn to_rust(&self) -> outpunch_client::ClientConfig {
|
|
41
|
+
let inner = self.0.lock().unwrap();
|
|
42
|
+
outpunch_client::ClientConfig {
|
|
43
|
+
server_url: inner.server_url.clone(),
|
|
44
|
+
secret: inner.secret.clone(),
|
|
45
|
+
service: inner.service.clone(),
|
|
46
|
+
forward_to: inner.forward_to.clone(),
|
|
47
|
+
reconnect_delay: Duration::from_secs_f64(inner.reconnect_delay),
|
|
48
|
+
request_timeout: Duration::from_secs_f64(inner.request_timeout),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn server_url(&self) -> String {
|
|
53
|
+
self.0.lock().unwrap().server_url.clone()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn set_server_url(&self, v: String) {
|
|
57
|
+
self.0.lock().unwrap().server_url = v;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn secret(&self) -> String {
|
|
61
|
+
self.0.lock().unwrap().secret.clone()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn set_secret(&self, v: String) {
|
|
65
|
+
self.0.lock().unwrap().secret = v;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn service(&self) -> String {
|
|
69
|
+
self.0.lock().unwrap().service.clone()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn set_service(&self, v: String) {
|
|
73
|
+
self.0.lock().unwrap().service = v;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fn forward_to(&self) -> String {
|
|
77
|
+
self.0.lock().unwrap().forward_to.clone()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fn set_forward_to(&self, v: String) {
|
|
81
|
+
self.0.lock().unwrap().forward_to = v;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fn reconnect_delay(&self) -> f64 {
|
|
85
|
+
self.0.lock().unwrap().reconnect_delay
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fn set_reconnect_delay(&self, v: f64) {
|
|
89
|
+
self.0.lock().unwrap().reconnect_delay = v;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fn request_timeout(&self) -> f64 {
|
|
93
|
+
self.0.lock().unwrap().request_timeout
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn set_request_timeout(&self, v: f64) {
|
|
97
|
+
self.0.lock().unwrap().request_timeout = v;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn inspect(&self) -> String {
|
|
101
|
+
let inner = self.0.lock().unwrap();
|
|
102
|
+
format!(
|
|
103
|
+
"#<Outpunch::ClientConfig server_url={:?} service={:?} forward_to={:?}>",
|
|
104
|
+
inner.server_url, inner.service, inner.forward_to
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fn run(_ruby: &Ruby, config: &ClientConfig) -> Result<(), Error> {
|
|
110
|
+
let rust_config = config.to_rust();
|
|
111
|
+
tokio::runtime::Builder::new_current_thread()
|
|
112
|
+
.enable_all()
|
|
113
|
+
.build()
|
|
114
|
+
.unwrap()
|
|
115
|
+
.block_on(outpunch_client::run(&rust_config));
|
|
116
|
+
Ok(())
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn run_connection(ruby: &Ruby, config: &ClientConfig) -> Result<(), Error> {
|
|
120
|
+
let rust_config = config.to_rust();
|
|
121
|
+
let result = tokio::runtime::Builder::new_current_thread()
|
|
122
|
+
.enable_all()
|
|
123
|
+
.build()
|
|
124
|
+
.unwrap()
|
|
125
|
+
.block_on(outpunch_client::run_connection(&rust_config));
|
|
126
|
+
result.map_err(|e: Box<dyn std::error::Error + Send + Sync>| {
|
|
127
|
+
Error::new(ruby.exception_runtime_error(), e.to_string())
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[magnus::init]
|
|
132
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
133
|
+
let module = ruby.define_module("Outpunch")?;
|
|
134
|
+
|
|
135
|
+
let config_class = module.define_class("ClientConfig", ruby.class_object())?;
|
|
136
|
+
config_class.define_singleton_method("new", function!(ClientConfig::new, 6))?;
|
|
137
|
+
config_class.define_method("server_url", method!(ClientConfig::server_url, 0))?;
|
|
138
|
+
config_class.define_method("server_url=", method!(ClientConfig::set_server_url, 1))?;
|
|
139
|
+
config_class.define_method("secret", method!(ClientConfig::secret, 0))?;
|
|
140
|
+
config_class.define_method("secret=", method!(ClientConfig::set_secret, 1))?;
|
|
141
|
+
config_class.define_method("service", method!(ClientConfig::service, 0))?;
|
|
142
|
+
config_class.define_method("service=", method!(ClientConfig::set_service, 1))?;
|
|
143
|
+
config_class.define_method("forward_to", method!(ClientConfig::forward_to, 0))?;
|
|
144
|
+
config_class.define_method("forward_to=", method!(ClientConfig::set_forward_to, 1))?;
|
|
145
|
+
config_class.define_method("reconnect_delay", method!(ClientConfig::reconnect_delay, 0))?;
|
|
146
|
+
config_class.define_method(
|
|
147
|
+
"reconnect_delay=",
|
|
148
|
+
method!(ClientConfig::set_reconnect_delay, 1),
|
|
149
|
+
)?;
|
|
150
|
+
config_class.define_method("request_timeout", method!(ClientConfig::request_timeout, 0))?;
|
|
151
|
+
config_class.define_method(
|
|
152
|
+
"request_timeout=",
|
|
153
|
+
method!(ClientConfig::set_request_timeout, 1),
|
|
154
|
+
)?;
|
|
155
|
+
config_class.define_method("inspect", method!(ClientConfig::inspect, 0))?;
|
|
156
|
+
config_class.define_method("to_s", method!(ClientConfig::inspect, 0))?;
|
|
157
|
+
|
|
158
|
+
module.define_module_function("run", function!(run, 1))?;
|
|
159
|
+
module.define_module_function("run_connection", function!(run_connection, 1))?;
|
|
160
|
+
|
|
161
|
+
server::init(ruby, &module)?;
|
|
162
|
+
|
|
163
|
+
Ok(())
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#[cfg(test)]
|
|
167
|
+
#[path = "lib_tests.rs"]
|
|
168
|
+
mod tests;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
use super::ClientConfig;
|
|
2
|
+
|
|
3
|
+
#[test]
|
|
4
|
+
fn client_config_defaults() {
|
|
5
|
+
let config = ClientConfig::new(
|
|
6
|
+
"wss://example.com/ws".into(),
|
|
7
|
+
"secret".into(),
|
|
8
|
+
"my-service".into(),
|
|
9
|
+
None,
|
|
10
|
+
None,
|
|
11
|
+
None,
|
|
12
|
+
);
|
|
13
|
+
let rust = config.to_rust();
|
|
14
|
+
assert_eq!(rust.forward_to, "http://localhost:8080");
|
|
15
|
+
assert_eq!(rust.reconnect_delay.as_secs_f64(), 5.0);
|
|
16
|
+
assert_eq!(rust.request_timeout.as_secs_f64(), 25.0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[test]
|
|
20
|
+
fn client_config_custom_values() {
|
|
21
|
+
let config = ClientConfig::new(
|
|
22
|
+
"wss://example.com/ws".into(),
|
|
23
|
+
"mysecret".into(),
|
|
24
|
+
"stormsnap".into(),
|
|
25
|
+
Some("http://localhost:8081".into()),
|
|
26
|
+
Some(10.0),
|
|
27
|
+
Some(30.0),
|
|
28
|
+
);
|
|
29
|
+
let rust = config.to_rust();
|
|
30
|
+
assert_eq!(rust.server_url, "wss://example.com/ws");
|
|
31
|
+
assert_eq!(rust.secret, "mysecret");
|
|
32
|
+
assert_eq!(rust.service, "stormsnap");
|
|
33
|
+
assert_eq!(rust.forward_to, "http://localhost:8081");
|
|
34
|
+
assert_eq!(rust.reconnect_delay.as_secs_f64(), 10.0);
|
|
35
|
+
assert_eq!(rust.request_timeout.as_secs_f64(), 30.0);
|
|
36
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
use std::sync::OnceLock;
|
|
3
|
+
use std::time::Duration;
|
|
4
|
+
|
|
5
|
+
use magnus::{Error, ExceptionClass, RHash, RModule, RString, Ruby, prelude::*, wrap};
|
|
6
|
+
use outpunch_core::protocol::{IncomingRequest, TunnelResponse};
|
|
7
|
+
use outpunch_core::server::{
|
|
8
|
+
Connection as CoreConnection, OutpunchServer, ServerConfig, TunnelError,
|
|
9
|
+
};
|
|
10
|
+
use tokio::runtime::Runtime;
|
|
11
|
+
use tokio::sync::{Mutex, mpsc};
|
|
12
|
+
|
|
13
|
+
use crate::gvl::without_gvl;
|
|
14
|
+
|
|
15
|
+
fn runtime() -> &'static Runtime {
|
|
16
|
+
static RUNTIME: OnceLock<Runtime> = OnceLock::new();
|
|
17
|
+
RUNTIME.get_or_init(|| {
|
|
18
|
+
tokio::runtime::Builder::new_multi_thread()
|
|
19
|
+
.enable_all()
|
|
20
|
+
.thread_name("outpunch")
|
|
21
|
+
.build()
|
|
22
|
+
.expect("failed to start outpunch runtime")
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn outpunch_error(ruby: &Ruby, name: &str) -> Result<ExceptionClass, Error> {
|
|
27
|
+
ruby.class_object()
|
|
28
|
+
.const_get::<_, RModule>("Outpunch")?
|
|
29
|
+
.const_get(name)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[wrap(class = "Outpunch::Server", free_immediately, size)]
|
|
33
|
+
pub struct Server {
|
|
34
|
+
inner: OutpunchServer,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
impl Server {
|
|
38
|
+
fn new(secret: String, timeout: f64, max_body_size: Option<usize>) -> Self {
|
|
39
|
+
let defaults = ServerConfig::default();
|
|
40
|
+
Self {
|
|
41
|
+
inner: OutpunchServer::new(ServerConfig {
|
|
42
|
+
secret,
|
|
43
|
+
timeout: Duration::from_secs_f64(timeout),
|
|
44
|
+
max_body_size: max_body_size.unwrap_or(defaults.max_body_size),
|
|
45
|
+
..defaults
|
|
46
|
+
}),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn create_connection(&self) -> Connection {
|
|
51
|
+
let inner = self.inner.create_connection();
|
|
52
|
+
let (tx, rx) = mpsc::unbounded_channel();
|
|
53
|
+
inner.on_message(move |msg| {
|
|
54
|
+
let _ = tx.send(msg);
|
|
55
|
+
});
|
|
56
|
+
Connection {
|
|
57
|
+
inner,
|
|
58
|
+
outgoing: Mutex::new(rx),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn is_connected(ruby: &Ruby, rb_self: &Self, service: String) -> Result<bool, Error> {
|
|
63
|
+
loop {
|
|
64
|
+
let server = rb_self.inner.clone();
|
|
65
|
+
let service = service.clone();
|
|
66
|
+
let result = without_gvl(ruby, move |cancel| {
|
|
67
|
+
runtime().block_on(async {
|
|
68
|
+
tokio::select! {
|
|
69
|
+
connected = server.is_connected(&service) => Some(connected),
|
|
70
|
+
_ = cancel.notified() => None,
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
})?;
|
|
74
|
+
if let Some(connected) = result {
|
|
75
|
+
return Ok(connected);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[allow(clippy::too_many_arguments)]
|
|
81
|
+
fn handle_request(
|
|
82
|
+
ruby: &Ruby,
|
|
83
|
+
rb_self: &Self,
|
|
84
|
+
service: String,
|
|
85
|
+
method: String,
|
|
86
|
+
path: String,
|
|
87
|
+
query: HashMap<String, String>,
|
|
88
|
+
headers: HashMap<String, String>,
|
|
89
|
+
body: Option<RString>,
|
|
90
|
+
) -> Result<RHash, Error> {
|
|
91
|
+
// SAFETY: the slice is copied before the GVL is released.
|
|
92
|
+
let body = body
|
|
93
|
+
.map(|b| String::from_utf8_lossy(unsafe { b.as_slice() }).into_owned())
|
|
94
|
+
.filter(|b| !b.is_empty());
|
|
95
|
+
let incoming = IncomingRequest {
|
|
96
|
+
service: service.clone(),
|
|
97
|
+
method,
|
|
98
|
+
path,
|
|
99
|
+
query,
|
|
100
|
+
headers,
|
|
101
|
+
body,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
let server = rb_self.inner.clone();
|
|
105
|
+
let result = without_gvl(ruby, move |cancel| {
|
|
106
|
+
runtime().block_on(async {
|
|
107
|
+
tokio::select! {
|
|
108
|
+
result = server.try_handle_request(incoming) => Some(result),
|
|
109
|
+
_ = cancel.notified() => None,
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
})?;
|
|
113
|
+
|
|
114
|
+
match result {
|
|
115
|
+
None => Err(Error::new(
|
|
116
|
+
ruby.exception_runtime_error(),
|
|
117
|
+
"tunnel request interrupted",
|
|
118
|
+
)),
|
|
119
|
+
Some(Ok(response)) => response_hash(ruby, response),
|
|
120
|
+
Some(Err(TunnelError::NotConnected)) => Err(Error::new(
|
|
121
|
+
outpunch_error(ruby, "NotConnectedError")?,
|
|
122
|
+
format!("Service '{service}' not connected"),
|
|
123
|
+
)),
|
|
124
|
+
Some(Err(TunnelError::Timeout)) => Err(Error::new(
|
|
125
|
+
outpunch_error(ruby, "TunnelTimeoutError")?,
|
|
126
|
+
format!("Tunnel timeout for service '{service}'"),
|
|
127
|
+
)),
|
|
128
|
+
Some(Err(err)) => Err(Error::new(
|
|
129
|
+
outpunch_error(ruby, "TunnelError")?,
|
|
130
|
+
err.to_string(),
|
|
131
|
+
)),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fn response_hash(ruby: &Ruby, response: TunnelResponse) -> Result<RHash, Error> {
|
|
137
|
+
let hash = ruby.hash_new();
|
|
138
|
+
hash.aset("request_id", response.request_id)?;
|
|
139
|
+
hash.aset("status", response.status)?;
|
|
140
|
+
hash.aset("headers", response.headers)?;
|
|
141
|
+
hash.aset("body", response.body)?;
|
|
142
|
+
hash.aset("body_encoding", response.body_encoding)?;
|
|
143
|
+
Ok(hash)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[wrap(class = "Outpunch::Connection", free_immediately, size)]
|
|
147
|
+
pub struct Connection {
|
|
148
|
+
inner: CoreConnection,
|
|
149
|
+
outgoing: Mutex<mpsc::UnboundedReceiver<String>>,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
impl Connection {
|
|
153
|
+
/// Feed one WebSocket text message from the tunnel client into the core.
|
|
154
|
+
fn push_message(ruby: &Ruby, rb_self: &Self, text: String) -> Result<(), Error> {
|
|
155
|
+
let inner = rb_self.inner.clone();
|
|
156
|
+
without_gvl(ruby, move |cancel| {
|
|
157
|
+
runtime().block_on(async {
|
|
158
|
+
tokio::select! {
|
|
159
|
+
_ = inner.push_message(text) => Some(()),
|
|
160
|
+
_ = cancel.notified() => None,
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
})?;
|
|
164
|
+
Ok(())
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// Block until the core has a message for the tunnel client. Returns nil once the
|
|
168
|
+
/// connection has ended.
|
|
169
|
+
fn next_outgoing(ruby: &Ruby, rb_self: &Self) -> Result<Option<String>, Error> {
|
|
170
|
+
loop {
|
|
171
|
+
let result = without_gvl(ruby, |cancel| {
|
|
172
|
+
runtime().block_on(async {
|
|
173
|
+
let mut outgoing = rb_self.outgoing.lock().await;
|
|
174
|
+
tokio::select! {
|
|
175
|
+
msg = outgoing.recv() => Some(msg),
|
|
176
|
+
_ = cancel.notified() => None,
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
})?;
|
|
180
|
+
if let Some(msg) = result {
|
|
181
|
+
return Ok(msg);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fn close(&self) {
|
|
187
|
+
self.inner.close();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Run the connection lifecycle (auth, relay, cleanup). Blocks until the
|
|
191
|
+
/// connection ends; an interrupt closes the connection.
|
|
192
|
+
fn run(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
|
|
193
|
+
let inner = rb_self.inner.clone();
|
|
194
|
+
without_gvl(ruby, move |cancel| {
|
|
195
|
+
runtime().block_on(async {
|
|
196
|
+
let run = inner.run();
|
|
197
|
+
tokio::pin!(run);
|
|
198
|
+
tokio::select! {
|
|
199
|
+
_ = &mut run => {}
|
|
200
|
+
_ = cancel.notified() => {
|
|
201
|
+
inner.close();
|
|
202
|
+
run.await;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
Some(())
|
|
206
|
+
})
|
|
207
|
+
})?;
|
|
208
|
+
Ok(())
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
pub fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
213
|
+
ruby.require("timeout")?;
|
|
214
|
+
let timeout_error: ExceptionClass = ruby
|
|
215
|
+
.class_object()
|
|
216
|
+
.const_get::<_, RModule>("Timeout")?
|
|
217
|
+
.const_get("Error")?;
|
|
218
|
+
module.define_error("NotConnectedError", ruby.exception_runtime_error())?;
|
|
219
|
+
module.define_error("TunnelError", ruby.exception_runtime_error())?;
|
|
220
|
+
module.define_error("TunnelTimeoutError", timeout_error)?;
|
|
221
|
+
|
|
222
|
+
let server = module.define_class("Server", ruby.class_object())?;
|
|
223
|
+
server.define_singleton_method("new", magnus::function!(Server::new, 3))?;
|
|
224
|
+
server.define_method(
|
|
225
|
+
"create_connection",
|
|
226
|
+
magnus::method!(Server::create_connection, 0),
|
|
227
|
+
)?;
|
|
228
|
+
server.define_method("connected?", magnus::method!(Server::is_connected, 1))?;
|
|
229
|
+
server.define_method("handle_request", magnus::method!(Server::handle_request, 6))?;
|
|
230
|
+
|
|
231
|
+
let connection = module.define_class("Connection", ruby.class_object())?;
|
|
232
|
+
connection.define_method("push_message", magnus::method!(Connection::push_message, 1))?;
|
|
233
|
+
connection.define_method(
|
|
234
|
+
"next_outgoing",
|
|
235
|
+
magnus::method!(Connection::next_outgoing, 0),
|
|
236
|
+
)?;
|
|
237
|
+
connection.define_method("close", magnus::method!(Connection::close, 0))?;
|
|
238
|
+
connection.define_method("run", magnus::method!(Connection::run, 0))?;
|
|
239
|
+
|
|
240
|
+
Ok(())
|
|
241
|
+
}
|
data/lib/outpunch.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: outpunch-ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.5.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- TechyCorp
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-28 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rb_sys
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.9'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.9'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake-compiler
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.2'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.2'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.12'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.12'
|
|
55
|
+
description:
|
|
56
|
+
email:
|
|
57
|
+
executables: []
|
|
58
|
+
extensions:
|
|
59
|
+
- ext/outpunch/extconf.rb
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- ext/outpunch/Cargo.toml
|
|
63
|
+
- ext/outpunch/extconf.rb
|
|
64
|
+
- ext/outpunch/src/gvl.rs
|
|
65
|
+
- ext/outpunch/src/lib.rs
|
|
66
|
+
- ext/outpunch/src/lib_tests.rs
|
|
67
|
+
- ext/outpunch/src/server.rs
|
|
68
|
+
- lib/outpunch-ruby.rb
|
|
69
|
+
- lib/outpunch.rb
|
|
70
|
+
homepage:
|
|
71
|
+
licenses:
|
|
72
|
+
- MIT
|
|
73
|
+
metadata: {}
|
|
74
|
+
post_install_message:
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.1'
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 3.5.22
|
|
90
|
+
signing_key:
|
|
91
|
+
specification_version: 4
|
|
92
|
+
summary: Ruby bindings for the outpunch tunnel client
|
|
93
|
+
test_files: []
|