forward-proxy 0.1.5 → 0.5.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 +5 -5
- data/CHANGELOG.md +4 -0
- data/README.md +3 -3
- data/exe/forward-proxy +3 -3
- data/lib/forward_proxy.rb +1 -1
- data/lib/forward_proxy/errors/http_method_not_implemented.rb +5 -0
- data/lib/forward_proxy/errors/http_parse_error.rb +5 -0
- data/lib/forward_proxy/server.rb +80 -49
- data/lib/forward_proxy/thread_pool.rb +7 -18
- data/lib/forward_proxy/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ccf19df114eeec55eaef8765999561904d8f5b6cd91b7ed2016053ec9e717408
|
|
4
|
+
data.tar.gz: ce712f520c0ed32b93ea211ff658d330bef7eeb26c2ddfcea1246d955ce78175
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b71608455b079161950b09521d8fd99496767463f3e2aa0f0d293f21e3955b03d7b0ba12f06f3b3df1ff6674d598ed2e7eec7492ffb8fd9047ebb19771625251
|
|
7
|
+
data.tar.gz: '092035474f162e29d2c7d0735b43cd38e41580aadca7ae50e97e02dee8e6f3825f50393497c0a8db8a5e0b3622d3e44e25e69e03ae02b56eba1cad55abdc1388'
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
![Gem Version][3] ![Gem][1] ![Build Status][2]
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Minimal forward proxy using 150LOC and only standard libraries. Useful for development, testing, and learning.
|
|
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
|
@@ -7,7 +7,7 @@ options = {}
|
|
|
7
7
|
OptionParser.new do |parser|
|
|
8
8
|
parser.banner = "Usage: forward-proxy [options]"
|
|
9
9
|
|
|
10
|
-
parser.on("-pPORT", "--port=PORT", "Bind to specified port. Default: 9292"
|
|
10
|
+
parser.on("-pPORT", "--port=PORT", String, "Bind to specified port. Default: 9292") do |bind_port|
|
|
11
11
|
options[:bind_port] = bind_port
|
|
12
12
|
end
|
|
13
13
|
|
|
@@ -15,7 +15,8 @@ OptionParser.new do |parser|
|
|
|
15
15
|
options[:bind_address] = bind_address
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
parser.on("-tTHREADS", "--threads=THREADS", Integer,
|
|
18
|
+
parser.on("-tTHREADS", "--threads=THREADS", Integer,
|
|
19
|
+
"Specify the number of client threads. Default: 128") do |threads|
|
|
19
20
|
options[:threads] = threads
|
|
20
21
|
end
|
|
21
22
|
|
|
@@ -26,6 +27,5 @@ OptionParser.new do |parser|
|
|
|
26
27
|
end.parse!
|
|
27
28
|
|
|
28
29
|
require_relative '../lib/forward_proxy'
|
|
29
|
-
|
|
30
30
|
server = ForwardProxy::Server.new(options)
|
|
31
31
|
server.start
|
data/lib/forward_proxy.rb
CHANGED
data/lib/forward_proxy/server.rb
CHANGED
|
@@ -1,64 +1,44 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'logger'
|
|
2
2
|
require 'socket'
|
|
3
3
|
require 'webrick'
|
|
4
4
|
require 'net/http'
|
|
5
|
+
require 'forward_proxy/errors/http_method_not_implemented'
|
|
6
|
+
require 'forward_proxy/errors/http_parse_error'
|
|
7
|
+
require 'forward_proxy/thread_pool'
|
|
5
8
|
|
|
6
9
|
module ForwardProxy
|
|
7
|
-
class HTTPMethodNotImplemented < StandardError; end
|
|
8
|
-
class HTTPParseError < StandardError; end
|
|
9
|
-
|
|
10
10
|
class Server
|
|
11
|
-
attr_reader :bind_address, :bind_port
|
|
11
|
+
attr_reader :bind_address, :bind_port, :logger
|
|
12
12
|
|
|
13
|
-
def initialize(bind_address: "127.0.0.1", bind_port: 9292, threads:
|
|
14
|
-
@thread_pool = ThreadPool.new(threads)
|
|
13
|
+
def initialize(bind_address: "127.0.0.1", bind_port: 9292, threads: 128, logger: default_logger)
|
|
15
14
|
@bind_address = bind_address
|
|
16
15
|
@bind_port = bind_port
|
|
16
|
+
@logger = logger
|
|
17
|
+
@thread_pool = ThreadPool.new(threads)
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def start
|
|
20
21
|
thread_pool.start
|
|
21
22
|
|
|
22
|
-
@
|
|
23
|
+
@socket = TCPServer.new(bind_address, bind_port)
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
logger.info("Listening #{bind_address}:#{bind_port}")
|
|
25
26
|
|
|
26
27
|
loop do
|
|
27
|
-
|
|
28
|
-
#
|
|
29
|
-
# accept(): POSIX.1-2001, POSIX.1-2008, SVr4, 4.4BSD (accept() first appeared in 4.2BSD).
|
|
30
|
-
#
|
|
31
|
-
# We see that POSIX.1-2008 is a viable reference (check this for a description of relevant
|
|
32
|
-
# standards for Linux systems). As already said in other answers, POSIX.1 standard specifies
|
|
33
|
-
# accept function as (POSIX-)thread safe (as defined in Base Definitions, section 3.399 Thread Safe)
|
|
34
|
-
# by not listing it on System Interfaces, section 2.9.1 Thread Safety.
|
|
35
|
-
thread_pool.schedule(server.accept) do |client_conn|
|
|
28
|
+
thread_pool.schedule(socket.accept) do |client_conn|
|
|
36
29
|
begin
|
|
37
30
|
req = parse_req(client_conn)
|
|
38
31
|
|
|
39
|
-
|
|
32
|
+
logger.info(req.request_line.strip)
|
|
40
33
|
|
|
41
34
|
case req.request_method
|
|
42
35
|
when METHOD_CONNECT then handle_tunnel(client_conn, req)
|
|
43
36
|
when METHOD_GET, METHOD_POST then handle(client_conn, req)
|
|
44
37
|
else
|
|
45
|
-
raise HTTPMethodNotImplemented
|
|
38
|
+
raise Errors::HTTPMethodNotImplemented
|
|
46
39
|
end
|
|
47
40
|
rescue => e
|
|
48
|
-
|
|
49
|
-
# "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content"
|
|
50
|
-
# https://tools.ietf.org/html/rfc7231#section-6.6.3
|
|
51
|
-
|
|
52
|
-
# The 502 (Bad Gateway) status code indicates that the server, while
|
|
53
|
-
# acting as a gateway or proxy, received an invalid response from an
|
|
54
|
-
# inbound server it accessed while attempting to fulfill the request.
|
|
55
|
-
client_conn.puts <<~eos.chomp
|
|
56
|
-
HTTP/1.1 502
|
|
57
|
-
Via: #{HEADER_VIA}
|
|
58
|
-
eos
|
|
59
|
-
|
|
60
|
-
puts e.message
|
|
61
|
-
puts e.backtrace.map { |line| " #{line}" }
|
|
41
|
+
handle_error(e, client_conn)
|
|
62
42
|
ensure
|
|
63
43
|
client_conn.close
|
|
64
44
|
end
|
|
@@ -67,22 +47,33 @@ module ForwardProxy
|
|
|
67
47
|
rescue Interrupt
|
|
68
48
|
shutdown
|
|
69
49
|
rescue IOError, Errno::EBADF => e
|
|
70
|
-
|
|
50
|
+
logger.error(e.message)
|
|
71
51
|
end
|
|
72
52
|
|
|
73
53
|
def shutdown
|
|
74
|
-
|
|
75
|
-
|
|
54
|
+
if socket
|
|
55
|
+
logger.info("Shutting down")
|
|
56
|
+
|
|
57
|
+
socket.close
|
|
58
|
+
end
|
|
76
59
|
end
|
|
77
60
|
|
|
78
61
|
private
|
|
79
62
|
|
|
80
|
-
attr_reader :
|
|
63
|
+
attr_reader :socket, :thread_pool
|
|
64
|
+
|
|
65
|
+
# The following comments are from the IETF document
|
|
66
|
+
# "Hypertext Transfer Protocol -- HTTP/1.1: Basic Rules"
|
|
67
|
+
# https://datatracker.ietf.org/doc/html/rfc2616#section-2.2
|
|
81
68
|
|
|
82
69
|
METHOD_CONNECT = "CONNECT"
|
|
83
70
|
METHOD_GET = "GET"
|
|
84
71
|
METHOD_POST = "POST"
|
|
85
72
|
|
|
73
|
+
# HTTP/1.1 defines the sequence CR LF as the end-of-line marker for all
|
|
74
|
+
# protocol elements except the entity-body.
|
|
75
|
+
HEADER_EOP = "\r\n"
|
|
76
|
+
|
|
86
77
|
# The following comments are from the IETF document
|
|
87
78
|
# "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content"
|
|
88
79
|
# https://tools.ietf.org/html/rfc7231#section-4.3.6
|
|
@@ -115,7 +106,10 @@ module ForwardProxy
|
|
|
115
106
|
# blank line that concludes the successful response's header section;
|
|
116
107
|
# data received after that blank line is from the server identified by
|
|
117
108
|
# the request-target.
|
|
118
|
-
client_conn.write
|
|
109
|
+
client_conn.write <<~eos.chomp
|
|
110
|
+
HTTP/1.1 200 OK
|
|
111
|
+
#{HEADER_EOP}
|
|
112
|
+
eos
|
|
119
113
|
|
|
120
114
|
# The CONNECT method requests that the recipient establish a tunnel to
|
|
121
115
|
# the destination origin server identified by the request-target and,
|
|
@@ -132,12 +126,38 @@ module ForwardProxy
|
|
|
132
126
|
def handle(client_conn, req)
|
|
133
127
|
Net::HTTP.start(req.host, req.port) do |http|
|
|
134
128
|
http.request(map_webrick_to_net_http_req(req)) do |resp|
|
|
129
|
+
# The following comments are from the IETF document
|
|
130
|
+
# "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content"
|
|
131
|
+
# https://tools.ietf.org/html/rfc7231#section-4.3.6
|
|
132
|
+
|
|
133
|
+
# An intermediary MAY combine an ordered subsequence of Via header
|
|
134
|
+
# field entries into a single such entry if the entries have identical
|
|
135
|
+
# received-protocol values. For example,
|
|
136
|
+
#
|
|
137
|
+
# Via: 1.0 ricky, 1.1 ethel, 1.1 fred, 1.0 lucy
|
|
138
|
+
#
|
|
139
|
+
# could be collapsed to
|
|
140
|
+
#
|
|
141
|
+
# Via: 1.0 ricky, 1.1 mertz, 1.0 lucy
|
|
142
|
+
#
|
|
143
|
+
# A sender SHOULD NOT combine multiple entries unless they are all
|
|
144
|
+
# under the same organizational control and the hosts have already been
|
|
145
|
+
# replaced by pseudonyms. A sender MUST NOT combine entries that have
|
|
146
|
+
# different received-protocol values.
|
|
147
|
+
headers = resp.to_hash.merge(Via: [HEADER_VIA, resp['Via']].compact.join(', '))
|
|
148
|
+
|
|
135
149
|
client_conn.puts <<~eos.chomp
|
|
136
150
|
HTTP/1.1 #{resp.code}
|
|
137
|
-
|
|
138
|
-
#{
|
|
151
|
+
#{headers.map { |header, value| "#{header}: #{value}" }.join("\n")}
|
|
152
|
+
#{HEADER_EOP}
|
|
139
153
|
eos
|
|
140
154
|
|
|
155
|
+
# The following comments are taken from:
|
|
156
|
+
# https://docs.ruby-lang.org/en/2.0.0/Net/HTTP.html#class-Net::HTTP-label-Streaming+Response+Bodies
|
|
157
|
+
|
|
158
|
+
# By default Net::HTTP reads an entire response into memory. If you are
|
|
159
|
+
# handling large files or wish to implement a progress bar you can
|
|
160
|
+
# instead stream the body directly to an IO.
|
|
141
161
|
resp.read_body do |chunk|
|
|
142
162
|
client_conn << chunk
|
|
143
163
|
end
|
|
@@ -145,6 +165,21 @@ module ForwardProxy
|
|
|
145
165
|
end
|
|
146
166
|
end
|
|
147
167
|
|
|
168
|
+
def handle_error(err, client_conn)
|
|
169
|
+
client_conn.puts <<~eos.chomp
|
|
170
|
+
HTTP/1.1 502
|
|
171
|
+
Via: #{HEADER_VIA}
|
|
172
|
+
#{HEADER_EOP}
|
|
173
|
+
eos
|
|
174
|
+
|
|
175
|
+
logger.error(err.message)
|
|
176
|
+
logger.debug(err.backtrace.join("\n"))
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def default_logger
|
|
180
|
+
Logger.new(STDOUT, level: :info)
|
|
181
|
+
end
|
|
182
|
+
|
|
148
183
|
def map_webrick_to_net_http_req(req)
|
|
149
184
|
req_headers = Hash[req.header.map { |k, v| [k, v.first] }]
|
|
150
185
|
|
|
@@ -152,16 +187,16 @@ module ForwardProxy
|
|
|
152
187
|
when METHOD_GET then Net::HTTP::Get
|
|
153
188
|
when METHOD_POST then Net::HTTP::Post
|
|
154
189
|
else
|
|
155
|
-
raise HTTPMethodNotImplemented
|
|
190
|
+
raise Errors::HTTPMethodNotImplemented
|
|
156
191
|
end
|
|
157
192
|
|
|
158
193
|
klass.new(req.path, req_headers)
|
|
159
194
|
end
|
|
160
195
|
|
|
161
|
-
def transfer(
|
|
196
|
+
def transfer(src_conn, dest_conn)
|
|
162
197
|
IO.copy_stream(src_conn, dest_conn)
|
|
163
198
|
rescue => e
|
|
164
|
-
|
|
199
|
+
logger.warn(e.message)
|
|
165
200
|
end
|
|
166
201
|
|
|
167
202
|
def parse_req(client_conn)
|
|
@@ -169,11 +204,7 @@ module ForwardProxy
|
|
|
169
204
|
req.parse(client_conn)
|
|
170
205
|
end
|
|
171
206
|
rescue => e
|
|
172
|
-
throw HTTPParseError.new(e.message)
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def log(str, level = 'INFO')
|
|
176
|
-
puts "[#{Time.now}] #{level} #{str}"
|
|
207
|
+
throw Errors::HTTPParseError.new(e.message)
|
|
177
208
|
end
|
|
178
209
|
end
|
|
179
210
|
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.5.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-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Forward proxy using just Ruby standard libraries.
|
|
14
14
|
email:
|
|
@@ -32,6 +32,8 @@ files:
|
|
|
32
32
|
- exe/forward-proxy
|
|
33
33
|
- forward-proxy.gemspec
|
|
34
34
|
- lib/forward_proxy.rb
|
|
35
|
+
- lib/forward_proxy/errors/http_method_not_implemented.rb
|
|
36
|
+
- lib/forward_proxy/errors/http_parse_error.rb
|
|
35
37
|
- lib/forward_proxy/server.rb
|
|
36
38
|
- lib/forward_proxy/thread_pool.rb
|
|
37
39
|
- lib/forward_proxy/version.rb
|
|
@@ -57,8 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
57
59
|
- !ruby/object:Gem::Version
|
|
58
60
|
version: '0'
|
|
59
61
|
requirements: []
|
|
60
|
-
|
|
61
|
-
rubygems_version: 2.5.2.3
|
|
62
|
+
rubygems_version: 3.0.3
|
|
62
63
|
signing_key:
|
|
63
64
|
specification_version: 4
|
|
64
65
|
summary: Forward proxy.
|