surro-gate 0.2.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Dependency Status](https://gemnasium.com/skateman/surro-gate.svg)](https://gemnasium.com/skateman/surro-gate)
|
6
6
|
[![Inline docs](http://inch-ci.org/github/skateman/surro-gate.svg?branch=master)](http://inch-ci.org/github/skateman/surro-gate)
|
7
7
|
[![Code Climate](https://codeclimate.com/github/skateman/surro-gate/badges/gpa.svg)](https://codeclimate.com/github/skateman/surro-gate)
|
8
|
-
[![codecov](https://codecov.io/gh/skateman/surro-gate/branch/master/graph/badge.svg)](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
|