forward-proxy 0.2.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 628359d25536c4190014c9e082fff41b2adef209f04a62d8a6b217c078f3b58b
4
- data.tar.gz: 7834932b7242fd66c120d270a922e385a6a58ab63c65e8ce2b7957da43ba5a11
3
+ metadata.gz: 7ef1fa7e497462fc27f6b33ace589450994664c35c0e2744fae3a46c6caa0372
4
+ data.tar.gz: a396b7121e8a512a518a033aaa14261d7b86fd0377cf6e29d183a34ba02ff010
5
5
  SHA512:
6
- metadata.gz: 8c47ab1d691807a23ba779f1ae8d4feecd32300f8d840044f66a5cc2f40ccd33c095db60ca9e3f99fb3c8fd60c02ae46d5d8d07b6a4de6298bef24731fb57cb4
7
- data.tar.gz: 609a35651496d98e4977f3691c7834efd58fe736615b3b4594e0bc61d11dad0088f11f535f6b287db85f95c1cc8ab55ea2a536447028b690d92ffd0aa3e60669
6
+ metadata.gz: af543772c2f4159b0697f3ad02d4a330f776b73f1ccbaab6e8eb0dff9a8b5a6855b515fea99165c884132e743fd65aff7ac5973916256bcd6f476102d9dad743
7
+ data.tar.gz: e625cc351e9ce99744ff6ad00dac9c4e2e22c75cc0f8e4899c17736dcd06067398e1228bbebc6fb5ba07b44322f9806e62259c712f7d9bfddc89db5620045ded
@@ -8,7 +8,7 @@ jobs:
8
8
 
9
9
  strategy:
10
10
  matrix:
11
- ruby: [ '2.3', '2.7' ]
11
+ ruby: [ '2.5', '2.7' ]
12
12
 
13
13
  steps:
14
14
  - uses: actions/checkout@v2
@@ -8,7 +8,7 @@ jobs:
8
8
 
9
9
  strategy:
10
10
  matrix:
11
- ruby: [ '2.3', '2.7' ]
11
+ ruby: [ '2.5', '2.7' ]
12
12
 
13
13
  steps:
14
14
  - uses: actions/checkout@v2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.6.0
4
+
5
+ - add connection timeout to stop tracking connection from saturating client threads.
6
+ - add cli flats for connection timeout `-t` and `--timeout`.
7
+ - change cli short flag `-t` to `-c` for `--threads`.
8
+
9
+ ## 0.5.0
10
+
11
+ - increase default threads from `32` to `128`.
12
+
3
13
  ## 0.2.0
4
14
 
5
15
  - Extract errors into module.
data/README.md CHANGED
@@ -6,8 +6,8 @@ Minimal forward proxy using 150LOC and only standard libraries. Useful for devel
6
6
 
7
7
  ```
8
8
  $ forward-proxy --binding 0.0.0.0 --port 3182 --threads 2
9
- [2021-01-14 19:37:47 +1100] INFO Listening 0.0.0.0:3182
10
- [2021-01-14 19:38:24 +1100] INFO CONNECT raw.githubusercontent.com:443 HTTP/1.1
9
+ I, [2021-07-04T10:33:32.947653 #1790] INFO -- : Listening 0.0.0.0:3182
10
+ I, [2021-07-04T10:33:32.998298 #1790] INFO -- : CONNECT raw.githubusercontent.com:443 HTTP/1.1
11
11
  ```
12
12
 
13
13
  ## Installation
data/exe/forward-proxy CHANGED
@@ -15,7 +15,13 @@ OptionParser.new do |parser|
15
15
  options[:bind_address] = bind_address
16
16
  end
17
17
 
18
- parser.on("-tTHREADS", "--threads=THREADS", Integer, "Specify the number of client threads. Default: 32") do |threads|
18
+ parser.on("-tTIMEOUT", "--timeout=TIMEOUT", Integer,
19
+ "Specify the connection timout in seconds. Default: 300") do |threads|
20
+ options[:timeout] = threads
21
+ end
22
+
23
+ parser.on("-cTHREADS", "--threads=THREADS", Integer,
24
+ "Specify the number of client threads. Default: 128") do |threads|
19
25
  options[:threads] = threads
20
26
  end
21
27
 
data/lib/forward_proxy.rb CHANGED
@@ -1,4 +1,4 @@
1
- $:.unshift File.dirname(__FILE__)
1
+ $LOAD_PATH.unshift File.dirname(__FILE__)
2
2
 
3
3
  require 'forward_proxy/version'
4
4
  require 'forward_proxy/server'
@@ -0,0 +1,5 @@
1
+ module ForwardProxy
2
+ module Errors
3
+ class ConnectionTimeoutError < StandardError; end
4
+ end
5
+ end
@@ -2,4 +2,4 @@ module ForwardProxy
2
2
  module Errors
3
3
  class HTTPMethodNotImplemented < StandardError; end
4
4
  end
5
- end
5
+ end
@@ -2,4 +2,4 @@ module ForwardProxy
2
2
  module Errors
3
3
  class HTTPParseError < StandardError; end
4
4
  end
5
- end
5
+ end
@@ -1,18 +1,23 @@
1
+ require 'logger'
1
2
  require 'socket'
2
- require 'webrick'
3
+ require 'timeout'
3
4
  require 'net/http'
5
+ require 'webrick'
6
+ require 'forward_proxy/errors/connection_timeout_error'
4
7
  require 'forward_proxy/errors/http_method_not_implemented'
5
8
  require 'forward_proxy/errors/http_parse_error'
6
9
  require 'forward_proxy/thread_pool'
7
10
 
8
11
  module ForwardProxy
9
12
  class Server
10
- attr_reader :bind_address, :bind_port
13
+ attr_reader :bind_address, :bind_port, :logger, :timeout
11
14
 
12
- def initialize(bind_address: "127.0.0.1", bind_port: 9292, threads: 32)
13
- @thread_pool = ThreadPool.new(threads)
15
+ def initialize(bind_address: "127.0.0.1", bind_port: 9292, threads: 4, timeout: 1, logger: default_logger)
14
16
  @bind_address = bind_address
15
17
  @bind_port = bind_port
18
+ @logger = logger
19
+ @thread_pool = ThreadPool.new(threads)
20
+ @timeout = timeout
16
21
  end
17
22
 
18
23
  def start
@@ -20,29 +25,25 @@ module ForwardProxy
20
25
 
21
26
  @socket = TCPServer.new(bind_address, bind_port)
22
27
 
23
- log("Listening #{bind_address}:#{bind_port}")
28
+ logger.info("Listening #{bind_address}:#{bind_port}")
24
29
 
25
30
  loop do
26
31
  thread_pool.schedule(socket.accept) do |client_conn|
27
32
  begin
28
- req = parse_req(client_conn)
33
+ Timeout::timeout(timeout, Errors::ConnectionTimeoutError, "connection exceeded #{timeout} seconds") do
34
+ req = parse_req(client_conn)
29
35
 
30
- log(req.request_line)
36
+ logger.info(req.request_line.strip)
31
37
 
32
- case req.request_method
33
- when METHOD_CONNECT then handle_tunnel(client_conn, req)
34
- when METHOD_GET, METHOD_POST then handle(client_conn, req)
35
- else
36
- raise Errors::HTTPMethodNotImplemented
38
+ case req.request_method
39
+ when METHOD_CONNECT then handle_tunnel(client_conn, req)
40
+ when METHOD_GET, METHOD_POST then handle(client_conn, req)
41
+ else
42
+ raise Errors::HTTPMethodNotImplemented
43
+ end
37
44
  end
38
45
  rescue => e
39
- client_conn.puts <<~eos.chomp
40
- HTTP/1.1 502
41
- Via: #{HEADER_VIA}
42
- eos
43
-
44
- puts e.message
45
- puts e.backtrace.map { |line| " #{line}" }
46
+ handle_error(client_conn, e)
46
47
  ensure
47
48
  client_conn.close
48
49
  end
@@ -51,14 +52,14 @@ module ForwardProxy
51
52
  rescue Interrupt
52
53
  shutdown
53
54
  rescue IOError, Errno::EBADF => e
54
- log(e.message, "ERROR")
55
+ logger.error(e.message)
55
56
  end
56
57
 
57
58
  def shutdown
58
59
  if socket
59
- log("Shutting down")
60
+ logger.info("Shutting down")
60
61
 
61
- socket.close
62
+ socket.close
62
63
  end
63
64
  end
64
65
 
@@ -66,10 +67,18 @@ module ForwardProxy
66
67
 
67
68
  attr_reader :socket, :thread_pool
68
69
 
70
+ # The following comments are from the IETF document
71
+ # "Hypertext Transfer Protocol -- HTTP/1.1: Basic Rules"
72
+ # https://datatracker.ietf.org/doc/html/rfc2616#section-2.2
73
+
69
74
  METHOD_CONNECT = "CONNECT"
70
75
  METHOD_GET = "GET"
71
76
  METHOD_POST = "POST"
72
77
 
78
+ # HTTP/1.1 defines the sequence CR LF as the end-of-line marker for all
79
+ # protocol elements except the entity-body.
80
+ HEADER_EOP = "\r\n"
81
+
73
82
  # The following comments are from the IETF document
74
83
  # "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content"
75
84
  # https://tools.ietf.org/html/rfc7231#section-4.3.6
@@ -102,7 +111,10 @@ module ForwardProxy
102
111
  # blank line that concludes the successful response's header section;
103
112
  # data received after that blank line is from the server identified by
104
113
  # the request-target.
105
- client_conn.write "HTTP/1.1 200 OK\n\n"
114
+ client_conn.write <<~eos.chomp
115
+ HTTP/1.1 200 OK
116
+ #{HEADER_EOP}
117
+ eos
106
118
 
107
119
  # The CONNECT method requests that the recipient establish a tunnel to
108
120
  # the destination origin server identified by the request-target and,
@@ -119,17 +131,37 @@ module ForwardProxy
119
131
  def handle(client_conn, req)
120
132
  Net::HTTP.start(req.host, req.port) do |http|
121
133
  http.request(map_webrick_to_net_http_req(req)) do |resp|
134
+ # The following comments are from the IETF document
135
+ # "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content"
136
+ # https://tools.ietf.org/html/rfc7231#section-4.3.6
137
+
138
+ # An intermediary MAY combine an ordered subsequence of Via header
139
+ # field entries into a single such entry if the entries have identical
140
+ # received-protocol values. For example,
141
+ #
142
+ # Via: 1.0 ricky, 1.1 ethel, 1.1 fred, 1.0 lucy
143
+ #
144
+ # could be collapsed to
145
+ #
146
+ # Via: 1.0 ricky, 1.1 mertz, 1.0 lucy
147
+ #
148
+ # A sender SHOULD NOT combine multiple entries unless they are all
149
+ # under the same organizational control and the hosts have already been
150
+ # replaced by pseudonyms. A sender MUST NOT combine entries that have
151
+ # different received-protocol values.
152
+ headers = resp.to_hash.merge(Via: [HEADER_VIA, resp['Via']].compact.join(', '))
153
+
122
154
  client_conn.puts <<~eos.chomp
123
155
  HTTP/1.1 #{resp.code}
124
- Via: #{[HEADER_VIA, resp['Via']].compact.join(', ')}
125
- #{resp.each.map { |header, value| "#{header}: #{value}" }.join("\n")}\n\n
156
+ #{headers.map { |header, value| "#{header}: #{value}" }.join("\n")}
157
+ #{HEADER_EOP}
126
158
  eos
127
159
 
128
- # The following comments are taken from:
160
+ # The following comments are taken from:
129
161
  # https://docs.ruby-lang.org/en/2.0.0/Net/HTTP.html#class-Net::HTTP-label-Streaming+Response+Bodies
130
-
162
+
131
163
  # By default Net::HTTP reads an entire response into memory. If you are
132
- # handling large files or wish to implement a progress bar you can
164
+ # handling large files or wish to implement a progress bar you can
133
165
  # instead stream the body directly to an IO.
134
166
  resp.read_body do |chunk|
135
167
  client_conn << chunk
@@ -138,6 +170,27 @@ module ForwardProxy
138
170
  end
139
171
  end
140
172
 
173
+ def handle_error(client_conn, err)
174
+ status_code = case err
175
+ when Errors::ConnectionTimeoutError then 504
176
+ else
177
+ 502
178
+ end
179
+
180
+ client_conn.puts <<~eos.chomp
181
+ HTTP/1.1 #{status_code}
182
+ Via: #{HEADER_VIA}
183
+ #{HEADER_EOP}
184
+ eos
185
+
186
+ logger.error(err.message)
187
+ logger.debug(err.backtrace.join("\n"))
188
+ end
189
+
190
+ def default_logger
191
+ Logger.new(STDOUT, level: :info)
192
+ end
193
+
141
194
  def map_webrick_to_net_http_req(req)
142
195
  req_headers = Hash[req.header.map { |k, v| [k, v.first] }]
143
196
 
@@ -154,7 +207,7 @@ module ForwardProxy
154
207
  def transfer(src_conn, dest_conn)
155
208
  IO.copy_stream(src_conn, dest_conn)
156
209
  rescue => e
157
- log(e.message, "WARNING")
210
+ logger.warn(e.message)
158
211
  end
159
212
 
160
213
  def parse_req(client_conn)
@@ -164,9 +217,5 @@ module ForwardProxy
164
217
  rescue => e
165
218
  throw Errors::HTTPParseError.new(e.message)
166
219
  end
167
-
168
- def log(str, level = 'INFO')
169
- puts "[#{Time.now}] #{level} #{str}"
170
- end
171
220
  end
172
221
  end
@@ -1,21 +1,18 @@
1
1
  module ForwardProxy
2
2
  class ThreadPool
3
- attr_reader :queue, :threads, :size
3
+ attr_reader :queue, :size
4
4
 
5
5
  def initialize(size)
6
- @size = size
7
- @queue = Queue.new
8
- @threads = []
6
+ @size = size
7
+ @queue = Queue.new
9
8
  end
10
9
 
11
10
  def start
12
11
  size.times do
13
- threads << Thread.new do
14
- catch(:exit) do
15
- loop do
16
- job, args = queue.pop
17
- job.call(*args)
18
- end
12
+ Thread.new do
13
+ loop do
14
+ job, args = queue.pop
15
+ job.call(*args)
19
16
  end
20
17
  end
21
18
  end
@@ -24,13 +21,5 @@ module ForwardProxy
24
21
  def schedule(*args, &block)
25
22
  queue.push([block, args])
26
23
  end
27
-
28
- def shutdown
29
- threads.each do
30
- schedule { throw :exit }
31
- end
32
-
33
- threads.each(&:join)
34
- end
35
24
  end
36
25
  end
@@ -1,3 +1,3 @@
1
1
  module ForwardProxy
2
- VERSION = "0.2.0"
2
+ VERSION = "0.6.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forward-proxy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Moriarty
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-05-06 00:00:00.000000000 Z
11
+ date: 2021-07-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Forward proxy using just Ruby standard libraries.
14
14
  email:
@@ -32,6 +32,7 @@ files:
32
32
  - exe/forward-proxy
33
33
  - forward-proxy.gemspec
34
34
  - lib/forward_proxy.rb
35
+ - lib/forward_proxy/errors/connection_timeout_error.rb
35
36
  - lib/forward_proxy/errors/http_method_not_implemented.rb
36
37
  - lib/forward_proxy/errors/http_parse_error.rb
37
38
  - lib/forward_proxy/server.rb