proxi 0.1 → 1.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
2
  SHA1:
3
- metadata.gz: 125abf0d6cecec0a987df416390f7d903f896ea2
4
- data.tar.gz: 524c9c84a6494d9838b448fa1723d0e7b838ebae
3
+ metadata.gz: 32e73fc0b66c9d84b526d7b819498c971141804b
4
+ data.tar.gz: 2708a278216649879f072473ebd6e16375ad0d5e
5
5
  SHA512:
6
- metadata.gz: dd8bf72dfb3dabd7088fbb5fc73d8cb02d6be8013659fda142e9f9ea439a5f4b876b434d22676c0b7616630bba6887768342720b602c0ed7b452647b4bb0acbd
7
- data.tar.gz: 4b10253015c9ab46428ae0494fa5a2d4188e5f5087e8f40e8d45355fdf26709d705c78f082b749055948f6f0c8dc31a2ae6816f934e0f75f1076fa274fc46f75
6
+ metadata.gz: f7ed6e53d8984ef3127443daf857d68aedc5a70f05c8548be9d3d7fdb58504c84ae37f4d73b9934e72aa3dd8d3cf79ebb8d2ad3f2e93abfeb2f3a63a2aef93e3
7
+ data.tar.gz: 0bf15ffa0343656882bf45df6c69e7b66fcf346d72db149c60ed0669e132a5bdd88bdf4a6958e1d6281ce4b8d10094d43485e27bec6f415bcf41206021b44809
data/.gitignore CHANGED
@@ -1 +1,4 @@
1
- scratch.rb
1
+ scratch.rb
2
+ .yardoc
3
+ proxi-0.1.gem
4
+ doc
data/README.md CHANGED
@@ -6,6 +6,7 @@ This is intended as a development tool, for when you want to see exactly what
6
6
  goes over the wire, want to log or save specific requests, responses, simulate
7
7
  timeouts or slow connections, etc.
8
8
 
9
+ For documentation see the [annotated source code](http://plexus.github.io/proxi/).
9
10
 
10
11
  ## License
11
12
 
@@ -0,0 +1,7 @@
1
+ cat lib/proxi.rb lib/proxi/server.rb lib/proxi/connection.rb lib/proxi/socket_factory.rb lib/proxi/reporting.rb lib/proxi/listeners.rb > doc/proxi
2
+
3
+ # Use https://github.com/Billiam/rocco , seems to at least mostly still work
4
+
5
+ RUBYOPT=-I~/github/rocco/lib ~/github/rocco/bin/rocco --docblocks doc/proxi
6
+
7
+ firefox-dev doc/proxi.html
@@ -1,13 +1,86 @@
1
1
  require 'socket'
2
2
  require 'thread'
3
- require 'optparse'
4
- require 'tmpdir'
5
3
  require 'openssl'
6
4
 
7
5
  require 'wisper'
8
6
 
7
+ # [Proxi](https://github.com/plexus/proxi) gives you flexible TCP/HTTP proxy
8
+ # servers for use during development.
9
+ #
10
+ # This can be useful when developing against external services, to see what goes
11
+ # over the wire, capture responses, or simulate timeouts.
12
+ #
13
+ # There are three main concepts in Proxi, `Server`, `Connection`, and `Socket`.
14
+ # A server listens locally for incoming requests. When it receives a request, it
15
+ # establishes a `Connection`, which opens a socket to the remote host, and then
16
+ # acts as a bidirectional pipe between the incoming and outgoing network
17
+ # sockets.
18
+ #
19
+ # To allow for multiple ways to handle the proxying, and multiple strategies for
20
+ # making remote connections, the `Server` does not create `Connections`
21
+ # directly, but instead delegates this to a "connection factory".
22
+ #
23
+ # A `Connection` in turn delegates how to open a remote socket to a "socket
24
+ # factory".
25
+ #
26
+ # Both Servers and Connections are observable, they emit events that objects can
27
+ # subscribe to.
28
+ #
29
+ # To use Proxi, hook up these factories, and register event listeners, and then
30
+ # start the server.
9
31
  module Proxi
32
+
33
+ # ## Basic examples
34
+ #
35
+ # These are provided for basic use cases, and as a starting point for more
36
+ # complex uses. They return the server instance, call `#call` to start the
37
+ # server, or use `on` or `subscribe` to listen to events.
38
+
39
+ # With `Proxy.tcp_proxy` you get basic proxying from a local port to a remote
40
+ # host and port, all bytes are simply forwarded without caring about their
41
+ # contents.
42
+ #
43
+ # For example:
44
+ #
45
+ # Proxi.tcp_proxy(3000, 'foo.example.com', 4000).call
46
+ def self.tcp_proxy(local_port, remote_host, remote_port)
47
+ reporter = ConsoleReporter.new
48
+
49
+ connection_factory = -> in_socket do
50
+ socket_factory = TCPSocketFactory.new(remote_host, remote_port)
51
+ Connection.new(in_socket, socket_factory).subscribe(reporter)
52
+ end
53
+
54
+ Server.new(local_port, connection_factory)
55
+ end
56
+
57
+ # `Proxi.http_host_proxy` allows proxying to multiple remote hosts, based on
58
+ # the HTTP `Host:` header. To use it, gather the IP addresses that correspond
59
+ # to each domain name, and provide this name-to-ip mapping to
60
+ # `http_host_proxy`. Now configure these domain names in `/etc/hosts` to point
61
+ # to the local host, so Proxi can intercept traffic intended for these
62
+ # domains.
63
+ #
64
+ # For example
65
+ #
66
+ # Proxi.http_host_proxy(80, {'foo.example.org' => '10.10.0.1'}).call
67
+ def self.http_host_proxy(local_port, host_mapping)
68
+ reporter = ConsoleReporter.new
69
+
70
+ connection_factory = -> in_socket do
71
+ socket_factory = HTTPHostSocketFactory.new(host_mapping)
72
+ Connection
73
+ .new(in_socket, socket_factory)
74
+ .subscribe(socket_factory, on: :data_in)
75
+ .subscribe(reporter)
76
+ end
77
+
78
+ Server.new(local_port, connection_factory)
79
+ end
10
80
  end
11
81
 
12
82
  require_relative 'proxi/server'
13
83
  require_relative 'proxi/connection'
84
+ require_relative 'proxi/socket_factory'
85
+ require_relative 'proxi/reporting'
86
+ require_relative 'proxi/listeners'
@@ -1,18 +1,45 @@
1
+ # ## Proxi::Connection
2
+ #
1
3
  module Proxi
4
+
5
+ # A `Connection` is a bidirectional pipe between two sockets.
6
+ #
7
+ # The proxy server hands it the socket for the incoming request from, and
8
+ # `Connection` then initiates an outgoing request, after which it forwards all
9
+ # traffic in both directions.
10
+ #
11
+ # Creating the outgoing request is delegated to a `Proxi::SocketFactory`. The
12
+ # reason being that the type of socket can vary (`TCPSocket`, `SSLSocket`), or
13
+ # there might be some logic involved to dispatch to the correct host, e.g.
14
+ # based on the HTTP Host header (cfr. `Proxi::HTTPHostSocketFactory`).
15
+ #
16
+ # A socket factory can subscribe to events to make informed decision, e.g. to
17
+ # inspect incoming data for HTTP headers.
18
+ #
19
+ # Proxi::Connection broadcasts the following events:
20
+ #
21
+ # - `start_connection(Proxi::Connection)`
22
+ # - `end_connection(Proxi::Connection)`
23
+ # - `main_loop_error(Proxi::Connection, Exception)`
24
+ # - `data_in(Proxi::Connection, Array)`
25
+ # - `data_out(Proxi::Connection, Array)`
2
26
  class Connection
3
27
  include Wisper::Publisher
4
28
 
5
- attr_reader :in_socket, :thread, :remote_host, :remote_port
29
+ attr_reader :in_socket, :thread
6
30
 
7
- def initialize(in_socket, remote_host, remote_port)
31
+ def initialize(in_socket, socket_factory, max_block_size: 4096)
8
32
  @in_socket = in_socket
9
- @remote_host, @remote_port = remote_host, remote_port
10
- @state = :new
33
+ @socket_factory = socket_factory
34
+ @max_block_size = max_block_size
11
35
  end
12
36
 
37
+ # `Connection#call` starts the connection handler thread. This is called by
38
+ # the server, and spawns a new Thread that handles the forwarding of data.
13
39
  def call
14
40
  broadcast(:start_connection, self)
15
41
  @thread = Thread.new { proxy_loop }
42
+ self
16
43
  end
17
44
 
18
45
  def alive?
@@ -26,11 +53,12 @@ module Proxi
26
53
  private
27
54
 
28
55
  def out_socket
29
- @out_socket ||= TCPSocket.new(remote_host, remote_port)
56
+ @out_socket ||= @socket_factory.call
30
57
  end
31
58
 
32
59
  def proxy_loop
33
60
  begin
61
+ handle_socket(in_socket)
34
62
  loop do
35
63
  begin
36
64
  ready_sockets.each do |socket|
@@ -55,7 +83,7 @@ module Proxi
55
83
  end
56
84
 
57
85
  def handle_socket(socket)
58
- data = socket.readpartial(4096)
86
+ data = socket.readpartial(@max_block_size)
59
87
 
60
88
  if socket == in_socket
61
89
  broadcast(:data_in, self, data)
@@ -69,67 +97,4 @@ module Proxi
69
97
  end
70
98
  end
71
99
 
72
- module SSLConnection
73
- def connect
74
- @out_socket = OpenSSL::SSL::SSLSocket.new(super)
75
- @out_socket.connect
76
- end
77
- end
78
-
79
- class HTTPConnection < Connection
80
- def initialize(in_socket, host_to_ip)
81
- @in_socket, @host_to_ip = in_socket, host_to_ip
82
- @new = true
83
- end
84
-
85
- def handle_socket(socket)
86
- data = socket.readpartial(4096)
87
-
88
- if socket == in_socket
89
- broadcast(:data_in, self, data)
90
-
91
- if @new
92
- @first_packet = data
93
- @new = false
94
- end
95
-
96
- out_socket.write data
97
- out_socket.flush
98
- else
99
- broadcast(:data_out, self, data)
100
- in_socket.write data
101
- in_socket.flush
102
- end
103
- end
104
-
105
- def ready_sockets
106
- if @new
107
- sockets = [in_socket]
108
- else
109
- sockets = [in_socket, out_socket]
110
- end
111
- IO.select(sockets).first
112
- end
113
-
114
- def out_socket
115
- host, port = @host_to_ip.fetch(headers["host"]).split(':')
116
- port ||= 80
117
- @out_socket ||= TCPSocket.new(host, port.to_i)
118
- end
119
-
120
- def headers
121
- Hash[
122
- @first_packet
123
- .sub(/\r\n\r\n.*/m, '')
124
- .each_line
125
- .drop(1) # GET / HTTP/1.1
126
- .map do |line|
127
- k,v = line.split(':', 2)
128
- [k.downcase, v.strip].tap do |header|
129
- broadcast(:header, self, *header)
130
- end
131
- end
132
- ]
133
- end
134
- end
135
100
  end
@@ -0,0 +1,50 @@
1
+ # ## Server Listeners
2
+ #
3
+ # These can be attached to a server to add extra behavior
4
+ #
5
+ module Proxi
6
+
7
+ # Log all incoming and outgoing traffic to files under `/tmp`
8
+ #
9
+ # For example:
10
+ #
11
+ # Proxi.tcp_server(...).subscribe(RequestResponseLogging.new).call
12
+ #
13
+ # The in and outgoing traffic will be captured per connection in
14
+ # `/tmp/proxi.1.in` and `/tmp/proxi.1.out`; `/tmp/proxi.2.in`, etc.
15
+ class RequestResponseLogging
16
+ def initialize(dir: Dir.tmpdir, name: "proxi")
17
+ @dir = dir
18
+ @name = name
19
+ @count = 0
20
+ @mutex = Mutex.new
21
+ end
22
+
23
+ def new_connection(connection)
24
+ count = @mutex.synchronize { @count += 1 }
25
+ in_fd = File.open(log_name(count, "in"), 'w')
26
+ out_fd = File.open(log_name(count, "out"), 'w')
27
+
28
+ connection
29
+ .on(:data_in) { |_, data| in_fd.write(data) ; in_fd.flush }
30
+ .on(:data_out) { |_, data| out_fd.write(data) ; out_fd.flush }
31
+ .on(:end_connection) { in_fd.close ; out_fd.close }
32
+ end
33
+
34
+ def log_name(num, suffix)
35
+ '%s/%s.%d.%s' % [@dir, @name, num, suffix]
36
+ end
37
+ end
38
+
39
+ # Wait before handing back data coming from the remote, this simulates a slow
40
+ # connection, and can be used to test timeouts.
41
+ class SlowDown
42
+ def initialize(wait_seconds: 5)
43
+ @wait_seconds = wait_seconds
44
+ end
45
+
46
+ def new_connection(connection)
47
+ connection.on(:data_out) { sleep @wait_seconds }
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,57 @@
1
+ # ### Reporting
2
+ #
3
+ # Proxi's server and connection classes don't have any logging or UI
4
+ # capabilities built in, but they broadcast events that we can listen to to perform these tasks.
5
+
6
+ module Proxi
7
+ # This is a very basic console reporter to see what's happening
8
+ #
9
+ # Subscribe to connection events, and you will see output that looks like this
10
+ #
11
+ # 1. +++
12
+ # 1. < 91
13
+ # 2. +++
14
+ # 3. +++
15
+ # 2. < 91
16
+ # 3. < 91
17
+ # 1. > 4096
18
+ # 1. > 3422
19
+ # 1. ---
20
+ #
21
+ # Each connection gets a unique incremental number assigned, followed by:
22
+ #
23
+ # - '+++' new connection
24
+ # - '---' connection closed
25
+ # - '< 1234' number of bytes proxied to the remote
26
+ # - '> 1234' number of bytes proxied back from the remote
27
+ class ConsoleReporter
28
+ def initialize
29
+ @count = 0
30
+ @mutex = Mutex.new
31
+ @connections = {}
32
+ end
33
+
34
+ def start_connection(conn)
35
+ @mutex.synchronize { @connections[conn] = (@count += 1) }
36
+ puts "#{@connections[conn]}. +++"
37
+ end
38
+
39
+ def end_connection(conn)
40
+ puts "#{@connections[conn]}. ---"
41
+ @connections.delete(conn)
42
+ end
43
+
44
+ def data_in(conn, data)
45
+ puts "#{@connections[conn]}. < #{data.size}"
46
+ end
47
+
48
+ def data_out(conn, data)
49
+ puts "#{@connections[conn]}. > #{data.size}"
50
+ end
51
+
52
+ def main_loop_error(conn, exc)
53
+ STDERR.puts "#{@connections[conn]}. #{exc.class} #{exc.message}"
54
+ STDERR.puts exc.backtrace
55
+ end
56
+ end
57
+ end
@@ -1,19 +1,45 @@
1
+ # ## Proxi::Server
2
+ #
1
3
  module Proxi
4
+
5
+ # `Proxi::Server` accepts TCP requests, and forwards them, by creating an
6
+ # outbound connection and forwarding traffic in both directions.
7
+ #
8
+ # The destination of the outbound connection, and the forwarding of data, is
9
+ # handled by a `Proxi::Connection`, created by a factory object, which can be a
10
+ # lambda.
11
+ #
12
+ # Start listening for connections by calling #call.
13
+ #
14
+ # `Proxi::Server` broadcasts the following events:
15
+ #
16
+ # - `new_connection(Proxi::Connection)`
17
+ # - `dead_connection(Proxi::Connection)`
2
18
  class Server
3
19
  include Wisper::Publisher
4
20
 
5
- MAX_CONNECTIONS = 5
6
-
7
- def initialize(listen_port, connection_factory)
21
+ # Public: Initialize a Server
22
+ #
23
+ # listen_port - The String or Integer of the port to listen to for
24
+ # incoming connections
25
+ # connection_factory - Implements #call(in_socket) and returns a
26
+ # Proxi::Connection
27
+ # max_connections - The maximum amount of parallel connections to handle
28
+ # at once
29
+ def initialize(listen_port, connection_factory, max_connections: 5)
30
+ @listen_port = listen_port
8
31
  @connection_factory = connection_factory
9
-
10
- @server = TCPServer.new(nil, listen_port)
11
-
32
+ @max_connections = 5
12
33
  @connections = []
13
34
  end
14
35
 
36
+ # Public: Start the server
37
+ #
38
+ # Start accepting and forwarding requests
15
39
  def call
16
- loop do
40
+ @server = TCPServer.new('localhost', @listen_port)
41
+
42
+ until @server.closed?
17
43
  in_socket = @server.accept
18
44
  connection = @connection_factory.call(in_socket)
19
45
 
@@ -21,22 +47,36 @@ module Proxi
21
47
 
22
48
  @connections.push(connection)
23
49
 
24
- connection.call
50
+ connection.call # spawns a new thread that handles proxying
25
51
 
26
52
  reap_connections
27
- while @connections.size >= MAX_CONNECTIONS
53
+ while @connections.size >= @max_connections
28
54
  sleep 1
29
55
  reap_connections
30
56
  end
31
57
  end
58
+ ensure
59
+ close unless @server.closed?
60
+ end
61
+
62
+ # Public: close the TCP server socket
63
+ #
64
+ # Included for completeness, note that if the proxy server is active it will
65
+ # likely be blocking on TCPServer#accept, and the server port will stay open
66
+ # until it has accepted one final request.
67
+ def close
68
+ @server.close
32
69
  end
33
70
 
71
+ private
72
+
34
73
  def reap_connections
35
- @connections = @connections.select do |t|
36
- if t.alive?
74
+ @connections = @connections.select do |conn|
75
+ if conn.alive?
37
76
  true
38
77
  else
39
- t.join_thread
78
+ broadcast(:dead_connection, conn)
79
+ conn.join_thread
40
80
  false
41
81
  end
42
82
  end
@@ -0,0 +1,92 @@
1
+ # ## Socket factories
2
+ #
3
+ module Proxi
4
+ # ### TCPSocketFactory
5
+ #
6
+ # This is the most vanilla type of socket factory.
7
+ #
8
+ # Suitable when all requests need to be forwarded to the same host and port.
9
+ class TCPSocketFactory
10
+ def initialize(remote_host, remote_port)
11
+ @remote_host, @remote_port = remote_host, remote_port
12
+ end
13
+
14
+ def call
15
+ TCPSocket.new(@remote_host, @remote_port)
16
+ end
17
+ end
18
+
19
+ # ### SSLSocketFactory
20
+ #
21
+ # This will set up an encrypted (SSL, https) connection to the target host.
22
+ # This way the proxy server communicates *unencrypted* locally, but
23
+ # encrypts/decrypts communication with the remote host.
24
+ #
25
+ # If you want to forward SSL connections as-is, use a `TCPSocketFactory`, in
26
+ # that case however you won't be able to inspect any data passing through,
27
+ # since it will be encrypted.
28
+ class SSLSocketFactory < TCPSocketFactory
29
+ def call
30
+ OpenSSL::SSL::SSLSocket.new(super).tap(&:connect)
31
+ end
32
+ end
33
+
34
+ # ### HTTPHostSocketFactory
35
+ #
36
+ # Dispatches HTTP traffic to multiple hosts, based on the HTTP `Host:` header.
37
+ #
38
+ # HTTPHostSocketFactory expects to receive data events from the connection, so
39
+ # make sure you subscribe it to connection events. (see `Proxi.http_proxy` for
40
+ # an example).
41
+ #
42
+ # To use this effectively, configure your local `/etc/hosts` so the relevant
43
+ # domains point to localhost. That way the proxy will be able to intercept
44
+ # them.
45
+ #
46
+ # This class is single use only! Create a new instance for each `Proxi::Connection`.
47
+ class HTTPHostSocketFactory
48
+
49
+ # Initialize a HTTPHostSocketFactory
50
+ #
51
+ # `host_mapping` - A Hash mapping hostnames to IP addresses, and, optionally, ports
52
+ #
53
+ # For example:
54
+ #
55
+ # HTTPHostSocketFactory.new(
56
+ # 'foo.example.com' => '10.10.10.1:8080',
57
+ # 'bar.example.com' => '10.10.10.2:8080'
58
+ # )
59
+ def initialize(host_mapping)
60
+ @host_mapping = host_mapping
61
+ end
62
+
63
+ # This is an event listener, it will be broadcast by the `Connection` whenever
64
+ # it gets new request data. We capture the first packet, assuming it
65
+ # contains the HTTP headers.
66
+ #
67
+ # `Connection` will only request an outgoing socket from us (call `#call`)
68
+ # after it received the initial request payload.
69
+ def data_in(connection, data)
70
+ @first_packet ||= data
71
+ end
72
+
73
+ def call
74
+ host, port = @host_to_ip.fetch(headers["host"]).split(':')
75
+ port ||= 80
76
+ TCPSocket.new(host, port.to_i)
77
+ end
78
+
79
+ def headers
80
+ Hash[
81
+ @first_packet
82
+ .sub(/\r\n\r\n.*/m, '')
83
+ .each_line
84
+ .drop(1) # GET / HTTP/1.1
85
+ .map do |line|
86
+ k,v = line.split(':', 2)
87
+ [k.downcase, v.strip]
88
+ end
89
+ ]
90
+ end
91
+ end
92
+ end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = 'proxi'
3
- gem.version = '0.1'
3
+ gem.version = '1.0'
4
4
  gem.authors = [ 'Arne Brasseur' ]
5
5
  gem.email = [ 'arne@arnebrasseur.net' ]
6
6
  gem.description = 'TCP and HTTP proxy scripts'
data/proxy.rb CHANGED
@@ -1,5 +1,12 @@
1
1
  #!/usr/bin/ruby
2
2
 
3
+ # This is my original proxy.rb script that served me well for many years, before
4
+ # I decided to clean it up and turn it into a gem.
5
+ #
6
+ # Have a look under lib/ for the much improved version. Leaving it here just a
7
+ # little longer for reference. There are still a few features that haven't been
8
+ # ported yet.
9
+
3
10
  require 'socket'
4
11
  require 'thread'
5
12
  require 'optparse'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: proxi
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: '1.0'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arne Brasseur
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-10-24 00:00:00.000000000 Z
11
+ date: 2015-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: wisper
@@ -51,9 +51,13 @@ files:
51
51
  - Gemfile.lock
52
52
  - LICENSE
53
53
  - README.md
54
+ - generate_docs
54
55
  - lib/proxi.rb
55
56
  - lib/proxi/connection.rb
57
+ - lib/proxi/listeners.rb
58
+ - lib/proxi/reporting.rb
56
59
  - lib/proxi/server.rb
60
+ - lib/proxi/socket_factory.rb
57
61
  - proxi.gemspec
58
62
  - proxy.rb
59
63
  homepage: https://github.com/plexus/proxi