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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: a27ce2403acd4f16e3d9d0e67209773563b24e9d
4
- data.tar.gz: d8eb8edee4f24b9b66c03cea8e0df9cb2f6542da
2
+ SHA256:
3
+ metadata.gz: 542c462ea407735d332f61fd2a665ec27c151069c9b670da220592b30c4895bc
4
+ data.tar.gz: 17cf01b8d553093d77afb8409b995d06ff0351893431d35cf92afa079ae87247
5
5
  SHA512:
6
- metadata.gz: '0801c6d8fa84ea67afab352c434fcbdcb56b0518ffe5886d372e371dc22ae35cc8de4571b23313ea18612d28c0d2f3e4889b4cee447573c1b8e64635a6ab8f0c'
7
- data.tar.gz: 9b0324b3ca7638b9869285ea0993994fbabbf297f293eb0bd10b416fb37b4e2c77bc01ca75c7e9b92b7db8f042a906540ca5a94ecdbf237bcd0083ae1d55cd24
6
+ metadata.gz: 06d2cbf0cfab641f3006ab85c7baf48977b2b2bb05514b92cce2e990b35e31b34054b0b985cb9cc746f235bc764946e1c2d958c713a41e68a5c967508ece795b
7
+ data.tar.gz: a613ef079f5d878d5785139f2b215561252160f5f2bc1097ce13dce0eca47f063ac06a52d1e335df62bb1e08ec1eb117f56ef74ec93b858d5cb4ae6e6a4143ba
data/.gitignore CHANGED
@@ -9,6 +9,7 @@
9
9
  /test/tmp/
10
10
  /test/version_tmp/
11
11
  /tmp/
12
+ /lib/**/*.so
12
13
 
13
14
  ## Documentation cache and generated files:
14
15
  /.yardoc/
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.0.0
6
- - 2.1.9
7
- - 2.2.6
8
- - 2.3.3
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 general purpose TCP-to-TCP proxy implemented in Ruby.
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 two TCP socket connections
36
- left = TCPSocket.new('localhost', 3333)
37
- right = TCPSocket.new('localhost', 2222)
38
-
39
- # Push the sockets to the proxy
40
- proxy.push(left, right)
41
-
42
- # Wait for the communication to finish
43
- proxy.wait
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
- task :default => :spec
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,4 @@
1
+ require 'mkmf'
2
+
3
+ have_header 'sys/epoll.h'
4
+ create_makefile 'surro-gate/selector_ext'
@@ -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
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SurroGate
2
- VERSION = '0.2.2'.freeze
4
+ VERSION = '1.0.0'.freeze
5
+ HAVE_EXT = RUBY_PLATFORM =~ /linux/ && !defined?(JRUBY_VERSION) && !ENV['SURRO_GATE_NOEXT']
3
6
  end
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
- # A generic purpose TCP-to-TCP proxy
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 Proxy instance
9
- # @return [Proxy]
14
+ # Initializes a new Selector instance
15
+ # @return [SurroGate::Selector]
10
16
  def new(logger = nil)
11
- Proxy.new(logger)
17
+ SurroGate::Selector.new(logger)
12
18
  end
13
19
  end
14
20
  end
data/surro-gate.gemspec CHANGED
@@ -1,5 +1,6 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
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 general purrpose TCP-to-TCP proxy written in Ruby'
13
- spec.description = 'A general purrpose TCP-to-TCP proxy written in Ruby'
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 'nio4r', '~> 1.2.0'
26
+ spec.add_dependency 'concurrent-ruby'
25
27
 
26
- spec.add_development_dependency 'bundler', '~> 1.13'
27
- spec.add_development_dependency 'codecov', '~> 0.1.0'
28
- spec.add_development_dependency 'nyan-cat-formatter', '~> 0.11'
29
- spec.add_development_dependency 'rake', '~> 10.0'
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.2.2
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: 2017-02-12 00:00:00.000000000 Z
11
+ date: 2018-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: nio4r
14
+ name: concurrent-ruby
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 1.2.0
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: 1.2.0
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: '1.13'
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: '1.13'
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: '10.0'
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: '10.0'
54
+ version: '0'
83
55
  - !ruby/object:Gem::Dependency
84
- name: rspec
56
+ name: rake-compiler
85
57
  requirement: !ruby/object:Gem::Requirement
86
58
  requirements:
87
- - - "~>"
59
+ - - ">="
88
60
  - !ruby/object:Gem::Version
89
- version: '3.0'
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: '3.0'
68
+ version: '0'
97
69
  - !ruby/object:Gem::Dependency
98
- name: simplecov
70
+ name: rspec
99
71
  requirement: !ruby/object:Gem::Requirement
100
72
  requirements:
101
- - - "~>"
73
+ - - ">="
102
74
  - !ruby/object:Gem::Version
103
- version: '0.12'
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.12'
111
- description: A general purrpose TCP-to-TCP proxy written in Ruby
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/proxy.rb
133
- - lib/surro-gate/proxy_error.rb
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.5.2
132
+ rubygems_version: 2.7.7
157
133
  signing_key:
158
134
  specification_version: 4
159
- summary: A general purrpose TCP-to-TCP proxy written in Ruby
135
+ summary: A generic purrpose TCP-to-TCP proxy in Ruby
160
136
  test_files: []
@@ -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
@@ -1,5 +0,0 @@
1
- module SurroGate
2
- # This error is raised when at least one of the pushed sockets is already registered
3
- class ProxyError < StandardError
4
- end
5
- end