forward-proxy 0.2.0 → 0.6.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
  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