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 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,4 @@
1
+ require "mkmf"
2
+ require "rb_sys/mkmf"
3
+
4
+ create_rust_makefile("outpunch/outpunch")
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "outpunch"
data/lib/outpunch.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "outpunch/outpunch"
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: []