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 +4 -4
- data/.github/workflows/ci.yaml +1 -1
- data/.github/workflows/cli.yaml +1 -1
- data/CHANGELOG.md +10 -0
- data/README.md +2 -2
- data/exe/forward-proxy +7 -1
- data/lib/forward_proxy.rb +1 -1
- data/lib/forward_proxy/errors/connection_timeout_error.rb +5 -0
- data/lib/forward_proxy/errors/http_method_not_implemented.rb +1 -1
- data/lib/forward_proxy/errors/http_parse_error.rb +1 -1
- data/lib/forward_proxy/server.rb +82 -33
- data/lib/forward_proxy/thread_pool.rb +7 -18
- data/lib/forward_proxy/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7ef1fa7e497462fc27f6b33ace589450994664c35c0e2744fae3a46c6caa0372
|
4
|
+
data.tar.gz: a396b7121e8a512a518a033aaa14261d7b86fd0377cf6e29d183a34ba02ff010
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af543772c2f4159b0697f3ad02d4a330f776b73f1ccbaab6e8eb0dff9a8b5a6855b515fea99165c884132e743fd65aff7ac5973916256bcd6f476102d9dad743
|
7
|
+
data.tar.gz: e625cc351e9ce99744ff6ad00dac9c4e2e22c75cc0f8e4899c17736dcd06067398e1228bbebc6fb5ba07b44322f9806e62259c712f7d9bfddc89db5620045ded
|
data/.github/workflows/ci.yaml
CHANGED
data/.github/workflows/cli.yaml
CHANGED
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-
|
10
|
-
[2021-
|
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("-
|
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
data/lib/forward_proxy/server.rb
CHANGED
@@ -1,18 +1,23 @@
|
|
1
|
+
require 'logger'
|
1
2
|
require 'socket'
|
2
|
-
require '
|
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:
|
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
|
-
|
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
|
-
|
33
|
+
Timeout::timeout(timeout, Errors::ConnectionTimeoutError, "connection exceeded #{timeout} seconds") do
|
34
|
+
req = parse_req(client_conn)
|
29
35
|
|
30
|
-
|
36
|
+
logger.info(req.request_line.strip)
|
31
37
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
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
|
-
|
55
|
+
logger.error(e.message)
|
55
56
|
end
|
56
57
|
|
57
58
|
def shutdown
|
58
59
|
if socket
|
59
|
-
|
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
|
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
|
-
|
125
|
-
#{
|
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
|
-
|
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, :
|
3
|
+
attr_reader :queue, :size
|
4
4
|
|
5
5
|
def initialize(size)
|
6
|
-
@size
|
7
|
-
@queue
|
8
|
-
@threads = []
|
6
|
+
@size = size
|
7
|
+
@queue = Queue.new
|
9
8
|
end
|
10
9
|
|
11
10
|
def start
|
12
11
|
size.times do
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
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.
|
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-
|
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
|