surro-gate 0.2.2 → 1.0.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 +5 -5
- data/.gitignore +1 -0
- data/.travis.yml +11 -4
- data/README.md +22 -11
- data/Rakefile +11 -1
- data/ext/surro-gate/extconf.rb +4 -0
- data/ext/surro-gate/selector_ext.c +203 -0
- data/ext/surro-gate/selector_ext.h +22 -0
- data/lib/surro-gate/pair.rb +17 -0
- data/lib/surro-gate/selector.rb +105 -0
- data/lib/surro-gate/version.rb +4 -1
- data/lib/surro-gate.rb +12 -6
- data/surro-gate.gemspec +11 -11
- metadata +35 -59
- data/lib/surro-gate/proxy.rb +0 -129
- data/lib/surro-gate/proxy_error.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 542c462ea407735d332f61fd2a665ec27c151069c9b670da220592b30c4895bc
|
4
|
+
data.tar.gz: 17cf01b8d553093d77afb8409b995d06ff0351893431d35cf92afa079ae87247
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 06d2cbf0cfab641f3006ab85c7baf48977b2b2bb05514b92cce2e990b35e31b34054b0b985cb9cc746f235bc764946e1c2d958c713a41e68a5c967508ece795b
|
7
|
+
data.tar.gz: a613ef079f5d878d5785139f2b215561252160f5f2bc1097ce13dce0eca47f063ac06a52d1e335df62bb1e08ec1eb117f56ef74ec93b858d5cb4ae6e6a4143ba
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,11 +1,18 @@
|
|
1
1
|
sudo: false
|
2
2
|
language: ruby
|
3
3
|
cache: bundler
|
4
|
+
os:
|
5
|
+
- linux
|
6
|
+
- osx
|
7
|
+
compiler:
|
8
|
+
- gcc
|
9
|
+
- clang
|
4
10
|
rvm:
|
5
|
-
- 2.
|
6
|
-
- 2.
|
7
|
-
- 2.
|
8
|
-
- 2.
|
11
|
+
- 2.2
|
12
|
+
- 2.3
|
13
|
+
- 2.4
|
14
|
+
- 2.5
|
15
|
+
- 2.6
|
9
16
|
before_install:
|
10
17
|
- "echo 'gem: --no-ri --no-rdoc --no-document' > ~/.gemrc"
|
11
18
|
- "gem install bundler -v 1.13.0"
|
data/README.md
CHANGED
@@ -5,9 +5,8 @@
|
|
5
5
|
[](https://gemnasium.com/skateman/surro-gate)
|
6
6
|
[](http://inch-ci.org/github/skateman/surro-gate)
|
7
7
|
[](https://codeclimate.com/github/skateman/surro-gate)
|
8
|
-
[](https://codecov.io/gh/skateman/surro-gate)
|
9
8
|
|
10
|
-
SurroGate is a
|
9
|
+
SurroGate is a generic purrpose TCP-to-TCP proxy for Ruby implemented using epoll.
|
11
10
|
|
12
11
|
## Installation
|
13
12
|
|
@@ -32,15 +31,27 @@ require 'surro-gate'
|
|
32
31
|
|
33
32
|
proxy = SurroGate.new
|
34
33
|
|
35
|
-
# Create
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
# Push the sockets to the proxy
|
40
|
-
proxy.push(
|
41
|
-
|
42
|
-
|
43
|
-
|
34
|
+
# Create a pair of TCP socket connections
|
35
|
+
sock_1 = TCPSocket.new('localhost', 1111)
|
36
|
+
sock_2 = TCPSocket.new('localhost', 2222)
|
37
|
+
|
38
|
+
# Push the pair of sockets to the proxy
|
39
|
+
proxy.push(sock_1, sock_2)
|
40
|
+
|
41
|
+
loop do
|
42
|
+
# Select with a 1 second timeout
|
43
|
+
proxy.select(1000)
|
44
|
+
|
45
|
+
# Do some hard work
|
46
|
+
proxy.each_ready do |left, right|
|
47
|
+
begin
|
48
|
+
right.write_nonblock(left.read_nonblock(4096))
|
49
|
+
rescue => ex
|
50
|
+
# ...
|
51
|
+
proxy.pop(left, right) # Remove the failed connection pair from the proxy
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
44
55
|
|
45
56
|
```
|
46
57
|
|
data/Rakefile
CHANGED
@@ -1,6 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'bundler/gem_tasks'
|
2
4
|
require 'rspec/core/rake_task'
|
5
|
+
require 'rake/extensiontask'
|
6
|
+
require 'surro-gate/version'
|
3
7
|
|
4
8
|
RSpec::Core::RakeTask.new(:spec)
|
5
9
|
|
6
|
-
|
10
|
+
Rake::ExtensionTask.new('surro-gate') do |ext|
|
11
|
+
ext.name = 'surro-gate/selector_ext'
|
12
|
+
end
|
13
|
+
|
14
|
+
task :default => SurroGate::HAVE_EXT ? %i[compile spec] : %i[clean spec]
|
15
|
+
|
16
|
+
CLEAN.include '**/*.o', '**/*.so', 'pkg', 'tmp'
|
@@ -0,0 +1,203 @@
|
|
1
|
+
#include "selector_ext.h"
|
2
|
+
|
3
|
+
static VALUE mSurroGate = Qnil;
|
4
|
+
static VALUE cSurroGate_Selector = Qnil;
|
5
|
+
static VALUE cSurroGate_Pair = Qnil;
|
6
|
+
|
7
|
+
void epoll_register(int *epoll, int socket, int ltr, int rtl) {
|
8
|
+
struct epoll_event ev;
|
9
|
+
ev.data.u64 = ((uint64_t)ltr) << 32 | rtl;
|
10
|
+
ev.events = EPOLLONESHOT | EPOLLIN | EPOLLOUT;
|
11
|
+
epoll_ctl(*epoll, EPOLL_CTL_ADD, socket, &ev);
|
12
|
+
}
|
13
|
+
|
14
|
+
void epoll_deregister(int *epoll, int socket) {
|
15
|
+
epoll_ctl(*epoll, EPOLL_CTL_DEL, socket, NULL);
|
16
|
+
}
|
17
|
+
|
18
|
+
void epoll_rearm(int *epoll, int socket, int ltr, int rtl, int events) {
|
19
|
+
struct epoll_event ev;
|
20
|
+
ev.data.u64 = ((uint64_t)ltr) << 32 | rtl;
|
21
|
+
ev.events = EPOLLONESHOT | events;
|
22
|
+
epoll_ctl(*epoll, EPOLL_CTL_MOD, socket, &ev);
|
23
|
+
}
|
24
|
+
|
25
|
+
static VALUE pairing_compare(VALUE pair, VALUE sockets) {
|
26
|
+
int i;
|
27
|
+
VALUE left = rb_iv_get(pair, "@left");
|
28
|
+
VALUE right = rb_iv_get(pair, "@right");
|
29
|
+
|
30
|
+
for (i=0; i<RARRAY_LEN(sockets); i++) { // sockets.each
|
31
|
+
VALUE item = rb_ary_entry(sockets, i);
|
32
|
+
if (left == item || right == item) {
|
33
|
+
return Qtrue;
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
return Qnil;
|
38
|
+
};
|
39
|
+
|
40
|
+
static VALUE pairing_iterate(VALUE pair, VALUE self, int argc, VALUE *argv) {
|
41
|
+
int *selector;
|
42
|
+
VALUE inverse, inv_idx;
|
43
|
+
|
44
|
+
// Yield only for the pairs that are ready
|
45
|
+
if (rb_funcall(pair, rb_intern("ready?"), 0) == Qtrue) {
|
46
|
+
rb_yield_values(2, rb_iv_get(pair, "@left"), rb_iv_get(pair, "@right")); // yield(pair.left, pair.right)
|
47
|
+
|
48
|
+
inv_idx = rb_iv_get(pair, "@inverse");
|
49
|
+
inverse = rb_funcall(rb_iv_get(self, "@pairing"), rb_intern("[]"), 1, inv_idx); // @pairing[inv_idx]
|
50
|
+
|
51
|
+
rb_iv_set(pair, "@rd_rdy", Qfalse);
|
52
|
+
rb_iv_set(pair, "@wr_rdy", Qfalse);
|
53
|
+
|
54
|
+
Data_Get_Struct(self, int, selector);
|
55
|
+
// Rearm left socket for reading and also writing if not ready for writing
|
56
|
+
epoll_rearm(selector, SOCK_PTR(rb_iv_get(pair, "@left")), NUM2INT(argv[1]), NUM2INT(inv_idx), EPOLLIN | (IVAR_TRUE(inverse, "@wr_rdy") ? 0 : EPOLLOUT));
|
57
|
+
// Rearm right socket for writing and also reading if not ready for reading
|
58
|
+
epoll_rearm(selector, SOCK_PTR(rb_iv_get(pair, "@right")), NUM2INT(inv_idx), NUM2INT(argv[1]), EPOLLOUT | (IVAR_TRUE(inverse, "@rd_rdy") ? 0 : EPOLLIN));
|
59
|
+
}
|
60
|
+
return Qnil;
|
61
|
+
}
|
62
|
+
|
63
|
+
static VALUE SurroGate_Selector_allocate(VALUE self) {
|
64
|
+
int *selector = malloc(sizeof(int));
|
65
|
+
|
66
|
+
if (selector != NULL) {
|
67
|
+
*selector = epoll_create1(0);
|
68
|
+
if (*selector > 0) {
|
69
|
+
return Data_Wrap_Struct(self, NULL, SurroGate_Selector_free, selector);
|
70
|
+
} else {
|
71
|
+
xfree(selector);
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
rb_raise(rb_eRuntimeError, "Allocation failed!");
|
76
|
+
return Qnil;
|
77
|
+
}
|
78
|
+
|
79
|
+
static void SurroGate_Selector_free(int *selector) {
|
80
|
+
close(*selector);
|
81
|
+
xfree(selector);
|
82
|
+
}
|
83
|
+
|
84
|
+
static VALUE SurroGate_Selector_initialize(VALUE self, VALUE logger) {
|
85
|
+
VALUE mConcurrent = rb_const_get(rb_cObject, rb_intern("Concurrent"));
|
86
|
+
VALUE cArray = rb_const_get(mConcurrent, rb_intern("Array"));
|
87
|
+
|
88
|
+
rb_iv_set(self, "@pairing", rb_class_new_instance(0, NULL, cArray)); // @pairing = Concurrent::Array.new
|
89
|
+
rb_iv_set(self, "@logger", logger);
|
90
|
+
|
91
|
+
return Qnil;
|
92
|
+
}
|
93
|
+
|
94
|
+
static VALUE SurroGate_Selector_push(VALUE self, VALUE left, VALUE right) {
|
95
|
+
int index_ltr, index_rtl, *selector;
|
96
|
+
|
97
|
+
VALUE pair_LTR[2] = {left, right};
|
98
|
+
VALUE pair_RTL[2] = {right, left};
|
99
|
+
VALUE left_to_right = rb_class_new_instance(2, pair_RTL, cSurroGate_Pair); // SurroGate::Pair.new(left, right)
|
100
|
+
VALUE right_to_left = rb_class_new_instance(2, pair_LTR, cSurroGate_Pair); // SurroGate::Pair.new(right, left)
|
101
|
+
|
102
|
+
VALUE pairing = rb_iv_get(self, "@pairing");
|
103
|
+
|
104
|
+
Check_Type(left, T_FILE);
|
105
|
+
Check_Type(right, T_FILE);
|
106
|
+
|
107
|
+
// raise ArgumentError if @pairing.detect(&pairing_compare)
|
108
|
+
if (rb_block_call(pairing, rb_intern("detect"), 0, NULL, pairing_compare, rb_ary_new_from_values(2, pair_LTR)) != Qnil) {
|
109
|
+
rb_raise(rb_eArgError, "Socket already registered!");
|
110
|
+
}
|
111
|
+
|
112
|
+
Data_Get_Struct(self, int, selector);
|
113
|
+
|
114
|
+
rb_funcall(pairing, rb_intern("push"), 2, left_to_right, right_to_left); // @pairing.push(left_to_right, right_to_left)
|
115
|
+
index_ltr = NUM2INT(rb_funcall(pairing, rb_intern("index"), 1, left_to_right)); // @pairing.index(left_to_right)
|
116
|
+
index_rtl = NUM2INT(rb_funcall(pairing, rb_intern("index"), 1, right_to_left)); // @pairing.index(right_to_left)
|
117
|
+
|
118
|
+
rb_iv_set(left_to_right, "@inverse", INT2NUM(index_rtl));
|
119
|
+
rb_iv_set(right_to_left, "@inverse", INT2NUM(index_ltr));
|
120
|
+
|
121
|
+
epoll_register(selector, SOCK_PTR(left), index_ltr, index_rtl);
|
122
|
+
epoll_register(selector, SOCK_PTR(right), index_rtl, index_ltr);
|
123
|
+
|
124
|
+
return Qtrue;
|
125
|
+
}
|
126
|
+
|
127
|
+
static VALUE SurroGate_Selector_pop(VALUE self, VALUE sockets) {
|
128
|
+
int i, *selector;
|
129
|
+
|
130
|
+
VALUE pairing = rb_iv_get(self, "@pairing");
|
131
|
+
rb_block_call(pairing, rb_intern("delete_if"), 0, NULL, pairing_compare, sockets); // @pairing.delete_if(&pairing_compare)
|
132
|
+
|
133
|
+
Data_Get_Struct(self, int, selector);
|
134
|
+
for (i=0; i<RARRAY_LEN(sockets); i++) {
|
135
|
+
epoll_deregister(selector, SOCK_PTR(rb_ary_entry(sockets, i)));
|
136
|
+
}
|
137
|
+
|
138
|
+
return Qnil;
|
139
|
+
}
|
140
|
+
|
141
|
+
static VALUE SurroGate_Selector_select(VALUE self, VALUE timeout) {
|
142
|
+
int i, count, *selector, source, target;
|
143
|
+
struct epoll_event events[256];
|
144
|
+
VALUE read, write, socket;
|
145
|
+
|
146
|
+
VALUE pairing = rb_iv_get(self, "@pairing");
|
147
|
+
Data_Get_Struct(self, int, selector);
|
148
|
+
|
149
|
+
count = epoll_wait(*selector, events, 256, NUM2INT(timeout));
|
150
|
+
|
151
|
+
for (i=0; i<count; i++) {
|
152
|
+
source = (int)((events[i].data.u64 & 0xFFFFFFFF00000000LL) >> 32);
|
153
|
+
target = (int)(events[i].data.u64 & 0xFFFFFFFFLL);
|
154
|
+
|
155
|
+
read = rb_funcall(pairing, rb_intern("[]"), 1, INT2NUM(target)); // @pairing[source]
|
156
|
+
write = rb_funcall(pairing, rb_intern("[]"), 1, INT2NUM(source)); // @pairing[target]
|
157
|
+
|
158
|
+
if (events[i].events & EPOLLIN && events[i].events & EPOLLOUT) {
|
159
|
+
// Socket is both available for read and write
|
160
|
+
rb_iv_set(read, "@rd_rdy", Qtrue); // read.rd_rdy = true
|
161
|
+
rb_iv_set(write, "@wr_rdy", Qtrue); // write.wr_rdy = true
|
162
|
+
} else if (events[i].events & EPOLLIN) {
|
163
|
+
// Socket is available for read, reregister it for write if not writable
|
164
|
+
rb_iv_set(read, "@rd_rdy", Qtrue); // read.rd_rdy = true
|
165
|
+
if (rb_iv_get(write, "@wr_rdy") == Qfalse) { // if !write.wr_rdy
|
166
|
+
socket = rb_iv_get(read, "@left"); // read.left
|
167
|
+
epoll_rearm(selector, SOCK_PTR(socket), target, source, EPOLLOUT);
|
168
|
+
}
|
169
|
+
} else if (events[i].events & EPOLLOUT) {
|
170
|
+
// Socket is available for write, reregister it for read if not readable
|
171
|
+
rb_iv_set(write, "@wr_rdy", Qtrue); // write.wr_rdy = true
|
172
|
+
if (rb_iv_get(write, "@rd_rdy") == Qfalse) { // if !source.rd_rdy
|
173
|
+
socket = rb_iv_get(write, "@right"); // write.right
|
174
|
+
epoll_rearm(selector, SOCK_PTR(socket), source, target, EPOLLIN);
|
175
|
+
}
|
176
|
+
}
|
177
|
+
}
|
178
|
+
|
179
|
+
return INT2NUM(count);
|
180
|
+
}
|
181
|
+
|
182
|
+
static VALUE SurroGate_Selector_each_ready(VALUE self) {
|
183
|
+
VALUE pairing = rb_iv_get(self, "@pairing");
|
184
|
+
rb_need_block();
|
185
|
+
return rb_block_call(pairing, rb_intern("each_with_index"), 0, NULL, pairing_iterate, self);
|
186
|
+
}
|
187
|
+
|
188
|
+
void Init_selector_ext() {
|
189
|
+
rb_require("concurrent");
|
190
|
+
rb_require("surro-gate/pair");
|
191
|
+
|
192
|
+
mSurroGate = rb_define_module("SurroGate");
|
193
|
+
cSurroGate_Selector = rb_define_class_under(mSurroGate, "Selector", rb_cObject);
|
194
|
+
cSurroGate_Pair = rb_const_get(mSurroGate, rb_intern("Pair"));
|
195
|
+
|
196
|
+
rb_define_alloc_func(cSurroGate_Selector, SurroGate_Selector_allocate);
|
197
|
+
|
198
|
+
rb_define_method(cSurroGate_Selector, "initialize", SurroGate_Selector_initialize, 1);
|
199
|
+
rb_define_method(cSurroGate_Selector, "push", SurroGate_Selector_push, 2);
|
200
|
+
rb_define_method(cSurroGate_Selector, "pop", SurroGate_Selector_pop, -2);
|
201
|
+
rb_define_method(cSurroGate_Selector, "select", SurroGate_Selector_select, 1);
|
202
|
+
rb_define_method(cSurroGate_Selector, "each_ready", SurroGate_Selector_each_ready, 0);
|
203
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#ifndef SELECTOR_EXT_H
|
2
|
+
#define SELECTOR_EXT_H
|
3
|
+
|
4
|
+
#include "ruby.h"
|
5
|
+
#include "ruby/io.h"
|
6
|
+
#include "stdlib.h"
|
7
|
+
#include "sys/epoll.h"
|
8
|
+
|
9
|
+
#define SOCK_PTR(X) RFILE(X)->fptr->fd
|
10
|
+
#define IVAR_TRUE(X, Y) rb_iv_get(X, Y) == Qtrue
|
11
|
+
|
12
|
+
static VALUE SurroGate_Selector_allocate(VALUE self);
|
13
|
+
static VALUE SurroGate_Selector_initialize(VALUE self, VALUE logger);
|
14
|
+
static VALUE SurroGate_Selector_push(VALUE self, VALUE left, VALUE right);
|
15
|
+
static VALUE SurroGate_Selector_select(VALUE self, VALUE timeout);
|
16
|
+
static VALUE SurroGate_Selector_each_ready(VALUE self);
|
17
|
+
|
18
|
+
static void SurroGate_Selector_free(int *epoll);
|
19
|
+
static VALUE pairing_compare(VALUE pair, VALUE sockets);
|
20
|
+
static VALUE pairing_iterate(VALUE pair, VALUE self, int argc, VALUE *argv);
|
21
|
+
|
22
|
+
#endif
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SurroGate
|
4
|
+
class Pair
|
5
|
+
attr_reader :left, :right
|
6
|
+
|
7
|
+
def initialize(left, right)
|
8
|
+
@left = left
|
9
|
+
@right = right
|
10
|
+
@rd_rdy = @wr_rdy = false
|
11
|
+
end
|
12
|
+
|
13
|
+
def ready?
|
14
|
+
@rd_rdy && @wr_rdy || @left.closed? || @right.closed?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'surro-gate/pair'
|
4
|
+
require 'concurrent'
|
5
|
+
|
6
|
+
module SurroGate
|
7
|
+
class Selector
|
8
|
+
def initialize(logger)
|
9
|
+
@logger = logger
|
10
|
+
|
11
|
+
@pairing = Concurrent::Array.new
|
12
|
+
@sockets = Concurrent::Array.new
|
13
|
+
@reads = Concurrent::Array.new
|
14
|
+
@writes = Concurrent::Array.new
|
15
|
+
|
16
|
+
@mutex = Mutex.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def push(left, right)
|
20
|
+
raise TypeError unless left.is_a?(IO) && right.is_a?(IO)
|
21
|
+
raise ArgumentError if @pairing.detect { |pair| [left, right].include?(pair.left) || [left, right].include?(pair.right) }
|
22
|
+
|
23
|
+
left_to_right = SurroGate::Pair.new(left, right)
|
24
|
+
right_to_left = SurroGate::Pair.new(right, left)
|
25
|
+
|
26
|
+
# The method can be called from a different thread
|
27
|
+
@mutex.synchronize do
|
28
|
+
@pairing.push(left_to_right, right_to_left)
|
29
|
+
|
30
|
+
@sockets.push(left, right)
|
31
|
+
@reads.push(left, right)
|
32
|
+
@writes.push(left, right)
|
33
|
+
end
|
34
|
+
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
def pop(*sockets)
|
39
|
+
[@sockets, @reads, @writes].each do |arr|
|
40
|
+
arr.delete_if { |sock| sockets.include?(sock) }
|
41
|
+
end
|
42
|
+
|
43
|
+
@pairing.delete_if { |pair| pairing_compare(pair, sockets) }
|
44
|
+
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def select(timeout)
|
49
|
+
begin
|
50
|
+
read, write, error = @mutex.synchronize { IO.select(@reads, @writes, @sockets, timeout * 0.001) }
|
51
|
+
rescue IOError
|
52
|
+
# One of the sockets is closed, Pair#ready? will catch it
|
53
|
+
end
|
54
|
+
|
55
|
+
error.to_a.each do
|
56
|
+
ltr = find_pairing(sock, :left)
|
57
|
+
rtl = find_pairing(sock, :right)
|
58
|
+
|
59
|
+
[ltr, rtl].each do |pair|
|
60
|
+
%i[@rd_rdy @wr_rdy].each do |ivar|
|
61
|
+
pair.instance_variable_set(ivar, true)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
read.to_a.each do |sock|
|
67
|
+
@reads.delete(sock)
|
68
|
+
find_pairing(sock, :left).instance_variable_set(:@rd_rdy, true)
|
69
|
+
end
|
70
|
+
|
71
|
+
write.to_a.each do |sock|
|
72
|
+
@writes.delete(sock)
|
73
|
+
find_pairing(sock, :right).instance_variable_set(:@wr_rdy, true)
|
74
|
+
end
|
75
|
+
|
76
|
+
read.to_a.length + write.to_a.length
|
77
|
+
end
|
78
|
+
|
79
|
+
def each_ready
|
80
|
+
@pairing.each do |pair|
|
81
|
+
next unless pair.ready?
|
82
|
+
|
83
|
+
yield(pair.left, pair.right)
|
84
|
+
|
85
|
+
pair.instance_variable_set(:@rd_rdy, false)
|
86
|
+
pair.instance_variable_set(:@wr_rdy, false)
|
87
|
+
|
88
|
+
@reads.push(pair.left)
|
89
|
+
@writes.push(pair.right)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def find_pairing(sock, direction)
|
96
|
+
@pairing.find { |pair| pair.send(direction) == sock }
|
97
|
+
end
|
98
|
+
|
99
|
+
def pairing_compare(pair, sockets)
|
100
|
+
sockets.each do |sock|
|
101
|
+
return true if [pair.left, pair.right].include?(sock)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/surro-gate/version.rb
CHANGED
data/lib/surro-gate.rb
CHANGED
@@ -1,14 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'surro-gate/version'
|
2
|
-
require 'surro-gate/proxy_error'
|
3
|
-
require 'surro-gate/proxy'
|
4
4
|
|
5
|
-
|
5
|
+
begin
|
6
|
+
require 'surro-gate/selector_ext'
|
7
|
+
rescue LoadError, NameError # Fall back to IO#select if epoll isn't available
|
8
|
+
require 'surro-gate/selector'
|
9
|
+
end
|
10
|
+
|
11
|
+
# A generic purrpose TCP-to-TCP proxy selector
|
6
12
|
module SurroGate
|
7
13
|
class << self
|
8
|
-
# Initializes a new
|
9
|
-
# @return [
|
14
|
+
# Initializes a new Selector instance
|
15
|
+
# @return [SurroGate::Selector]
|
10
16
|
def new(logger = nil)
|
11
|
-
|
17
|
+
SurroGate::Selector.new(logger)
|
12
18
|
end
|
13
19
|
end
|
14
20
|
end
|
data/surro-gate.gemspec
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require 'surro-gate/version'
|
5
6
|
|
@@ -9,24 +10,23 @@ Gem::Specification.new do |spec|
|
|
9
10
|
spec.authors = ['Dávid Halász']
|
10
11
|
spec.email = ['skateman@skateman.eu']
|
11
12
|
|
12
|
-
spec.summary = 'A
|
13
|
-
spec.description = 'A
|
13
|
+
spec.summary = 'A generic purrpose TCP-to-TCP proxy in Ruby'
|
14
|
+
spec.description = 'A generic purrpose TCP-to-TCP proxy for Ruby implemented using epoll'
|
14
15
|
spec.homepage = 'https://github.com/skateman/surro-gate'
|
15
16
|
spec.license = 'MIT'
|
16
17
|
|
17
18
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
19
|
f.match(%r{^(test|spec|features)/})
|
19
20
|
end
|
21
|
+
spec.extensions = ['ext/surro-gate/extconf.rb'] if SurroGate::HAVE_EXT
|
20
22
|
spec.bindir = 'exe'
|
21
23
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
24
|
spec.require_paths = ['lib']
|
23
25
|
|
24
|
-
spec.add_dependency '
|
26
|
+
spec.add_dependency 'concurrent-ruby'
|
25
27
|
|
26
|
-
spec.add_development_dependency 'bundler'
|
27
|
-
spec.add_development_dependency '
|
28
|
-
spec.add_development_dependency '
|
29
|
-
spec.add_development_dependency '
|
30
|
-
spec.add_development_dependency 'rspec', '~> 3.0'
|
31
|
-
spec.add_development_dependency 'simplecov', '~> 0.12'
|
28
|
+
spec.add_development_dependency 'bundler'
|
29
|
+
spec.add_development_dependency 'rake'
|
30
|
+
spec.add_development_dependency 'rake-compiler'
|
31
|
+
spec.add_development_dependency 'rspec'
|
32
32
|
end
|
metadata
CHANGED
@@ -1,118 +1,91 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: surro-gate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dávid Halász
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-08-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: concurrent-ruby
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: '0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: codecov
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: 0.1.0
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: 0.1.0
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: nyan-cat-formatter
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0.11'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0.11'
|
40
|
+
version: '0'
|
69
41
|
- !ruby/object:Gem::Dependency
|
70
42
|
name: rake
|
71
43
|
requirement: !ruby/object:Gem::Requirement
|
72
44
|
requirements:
|
73
|
-
- - "
|
45
|
+
- - ">="
|
74
46
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
47
|
+
version: '0'
|
76
48
|
type: :development
|
77
49
|
prerelease: false
|
78
50
|
version_requirements: !ruby/object:Gem::Requirement
|
79
51
|
requirements:
|
80
|
-
- - "
|
52
|
+
- - ">="
|
81
53
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
54
|
+
version: '0'
|
83
55
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
56
|
+
name: rake-compiler
|
85
57
|
requirement: !ruby/object:Gem::Requirement
|
86
58
|
requirements:
|
87
|
-
- - "
|
59
|
+
- - ">="
|
88
60
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
61
|
+
version: '0'
|
90
62
|
type: :development
|
91
63
|
prerelease: false
|
92
64
|
version_requirements: !ruby/object:Gem::Requirement
|
93
65
|
requirements:
|
94
|
-
- - "
|
66
|
+
- - ">="
|
95
67
|
- !ruby/object:Gem::Version
|
96
|
-
version: '
|
68
|
+
version: '0'
|
97
69
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
70
|
+
name: rspec
|
99
71
|
requirement: !ruby/object:Gem::Requirement
|
100
72
|
requirements:
|
101
|
-
- - "
|
73
|
+
- - ">="
|
102
74
|
- !ruby/object:Gem::Version
|
103
|
-
version: '0
|
75
|
+
version: '0'
|
104
76
|
type: :development
|
105
77
|
prerelease: false
|
106
78
|
version_requirements: !ruby/object:Gem::Requirement
|
107
79
|
requirements:
|
108
|
-
- - "
|
80
|
+
- - ">="
|
109
81
|
- !ruby/object:Gem::Version
|
110
|
-
version: '0
|
111
|
-
description: A
|
82
|
+
version: '0'
|
83
|
+
description: A generic purrpose TCP-to-TCP proxy for Ruby implemented using epoll
|
112
84
|
email:
|
113
85
|
- skateman@skateman.eu
|
114
86
|
executables: []
|
115
|
-
extensions:
|
87
|
+
extensions:
|
88
|
+
- ext/surro-gate/extconf.rb
|
116
89
|
extra_rdoc_files: []
|
117
90
|
files:
|
118
91
|
- ".codeclimate.yml"
|
@@ -128,9 +101,12 @@ files:
|
|
128
101
|
- bin/console
|
129
102
|
- bin/setup
|
130
103
|
- codecov.yml
|
104
|
+
- ext/surro-gate/extconf.rb
|
105
|
+
- ext/surro-gate/selector_ext.c
|
106
|
+
- ext/surro-gate/selector_ext.h
|
131
107
|
- lib/surro-gate.rb
|
132
|
-
- lib/surro-gate/
|
133
|
-
- lib/surro-gate/
|
108
|
+
- lib/surro-gate/pair.rb
|
109
|
+
- lib/surro-gate/selector.rb
|
134
110
|
- lib/surro-gate/version.rb
|
135
111
|
- surro-gate.gemspec
|
136
112
|
homepage: https://github.com/skateman/surro-gate
|
@@ -153,8 +129,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
153
129
|
version: '0'
|
154
130
|
requirements: []
|
155
131
|
rubyforge_project:
|
156
|
-
rubygems_version: 2.
|
132
|
+
rubygems_version: 2.7.7
|
157
133
|
signing_key:
|
158
134
|
specification_version: 4
|
159
|
-
summary: A
|
135
|
+
summary: A generic purrpose TCP-to-TCP proxy in Ruby
|
160
136
|
test_files: []
|
data/lib/surro-gate/proxy.rb
DELETED
@@ -1,129 +0,0 @@
|
|
1
|
-
require 'nio'
|
2
|
-
require 'logger'
|
3
|
-
|
4
|
-
module SurroGate
|
5
|
-
# This class is responsible for connecting TCP socket pairs and proxying between them.
|
6
|
-
#
|
7
|
-
# It uses a lazily-forked thread to handle the non blocking read and write operations.
|
8
|
-
# If one of the sockets get closed, the proxy automatically cleans up by deregistering
|
9
|
-
# and closing its pair. When the last socket pair gets cleaned up, the internal thread
|
10
|
-
# is killed automatically.
|
11
|
-
#
|
12
|
-
# The proxy was designed to be highly reusable and it can handle multiple socket pairs.
|
13
|
-
class Proxy
|
14
|
-
def initialize(logger)
|
15
|
-
@mutex = Mutex.new
|
16
|
-
@reader = NIO::Selector.new
|
17
|
-
@writer = NIO::Selector.new
|
18
|
-
@selectors = [@reader, @writer]
|
19
|
-
@log = logger || Logger.new(STDOUT)
|
20
|
-
end
|
21
|
-
|
22
|
-
# Registers a pair of socket for proxying.
|
23
|
-
#
|
24
|
-
# It also forks the internal thread if it is not running yet.
|
25
|
-
#
|
26
|
-
# @raise [ProxyError] when at least one of the pushed sockets is already registered
|
27
|
-
# @param left [TCPSocket]
|
28
|
-
# @param right [TCPSocket]
|
29
|
-
# @yield The block responsible for additional cleanup
|
30
|
-
# @return the registered socket pair as an array
|
31
|
-
def push(left, right, &block)
|
32
|
-
raise ProxyError, 'Socket already handled by the proxy' if includes?(left, right)
|
33
|
-
|
34
|
-
@log.info("Connecting #{left} <-> #{right}")
|
35
|
-
|
36
|
-
@mutex.synchronize do
|
37
|
-
proxy(left, right, block)
|
38
|
-
end
|
39
|
-
|
40
|
-
[left, right]
|
41
|
-
end
|
42
|
-
|
43
|
-
# Blocking wait until the internal thread is doing something useful.
|
44
|
-
def wait
|
45
|
-
@thread.join if alive?
|
46
|
-
end
|
47
|
-
|
48
|
-
# Determine if the internal thread is currently running or not.
|
49
|
-
def alive?
|
50
|
-
!@thread.nil? && @thread.alive?
|
51
|
-
end
|
52
|
-
|
53
|
-
private
|
54
|
-
|
55
|
-
def proxy(left, right, block = nil)
|
56
|
-
# Register the proxying in both directions
|
57
|
-
[[left, right], [right, left]].each do |rd, wr|
|
58
|
-
# Set up monitors for read/write separately
|
59
|
-
src = @reader.register(rd, :r)
|
60
|
-
dst = @writer.register(wr, :w)
|
61
|
-
|
62
|
-
# Set up handlers for the reader monitor
|
63
|
-
src.value = proc do
|
64
|
-
# Clean up the connection if one of the endpoints gets closed
|
65
|
-
cleanup(src.io, dst.io, &block) if src.io.closed? || dst.io.closed?
|
66
|
-
# Do the transmission and return with the bytes transferred
|
67
|
-
transmit(src.io, dst.io, block) if src.readable? && dst.writable?
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
thread_start unless @reader.empty? || @writer.empty?
|
72
|
-
end
|
73
|
-
|
74
|
-
def transmit(src, dst, block)
|
75
|
-
dst.write_nonblock(src.read_nonblock(4096))
|
76
|
-
rescue # Clean up both sockets if something bad happens
|
77
|
-
@log.warn("Transmission failure between #{src} <-> #{dst}")
|
78
|
-
cleanup(src, dst, &block)
|
79
|
-
end
|
80
|
-
|
81
|
-
def cleanup(*sockets)
|
82
|
-
# Deregister and close the sockets
|
83
|
-
sockets.each do |socket|
|
84
|
-
@selectors.each { |selector| selector.deregister(socket) if selector.registered?(socket) }
|
85
|
-
socket.close unless socket.closed?
|
86
|
-
end
|
87
|
-
|
88
|
-
@log.info("Disconnecting #{sockets.join(' <-> ')}")
|
89
|
-
|
90
|
-
yield if block_given?
|
91
|
-
|
92
|
-
# Make sure that the internal thread is stopped if no sockets remain
|
93
|
-
thread_stop if @reader.empty? && @writer.empty?
|
94
|
-
end
|
95
|
-
|
96
|
-
def thread_start
|
97
|
-
@log.debug('Starting the internal thread')
|
98
|
-
@thread ||= Thread.new do
|
99
|
-
loop do
|
100
|
-
reactor
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
def thread_stop
|
106
|
-
return if @thread.nil?
|
107
|
-
@log.debug('Stopping the internal thread')
|
108
|
-
thread = @thread
|
109
|
-
@thread = nil
|
110
|
-
thread.kill
|
111
|
-
end
|
112
|
-
|
113
|
-
def reactor
|
114
|
-
# Atomically get an array of readable monitors while also polling for writables
|
115
|
-
monitors = @mutex.synchronize do
|
116
|
-
@writer.select(0.1)
|
117
|
-
@reader.select(0.1) || []
|
118
|
-
end
|
119
|
-
# Call each transmission proc and collect the results
|
120
|
-
monitors.map { |m| m.value.call }
|
121
|
-
end
|
122
|
-
|
123
|
-
def includes?(*sockets)
|
124
|
-
sockets.any? do |socket|
|
125
|
-
@selectors.any? { |selector| selector.registered?(socket) }
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|