forward-proxy 0.1.3 → 0.4.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/.github/workflows/cli.yaml +25 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +1 -0
- data/README.md +1 -1
- data/Rakefile +19 -0
- data/exe/forward-proxy +1 -2
- 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 +98 -45
- data/lib/forward_proxy/version.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1d2b9dbd7abcfa81bcdbd29b6dd2045e6d2210e92b303f7cfcdc4ab35ab0b868
|
4
|
+
data.tar.gz: 729d4b7d964c8a5a3bec860f6bceb669a12225900a01b4737de8fdf6ba02a049
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 70c0b51f0313e4ffedd1146ae93035db45bc8fe3a7cee647ea11cb0397d6862e99047befbc39aee6a08b17fd1e3d16394eb59ad9ad092140ea9354da10feadf6
|
7
|
+
data.tar.gz: b0f3587f3c08b255be222f8f59530e19b5fcc6e589f93eafb1eaa21fac832a0f01847dfcf309ed443e51fdafd96a516241143c1a75795b3d09f0ce7088eedac0
|
@@ -0,0 +1,25 @@
|
|
1
|
+
name: Command Line Interface
|
2
|
+
|
3
|
+
on: [pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
|
9
|
+
strategy:
|
10
|
+
matrix:
|
11
|
+
ruby: [ '2.3', '2.7' ]
|
12
|
+
|
13
|
+
steps:
|
14
|
+
- uses: actions/checkout@v2
|
15
|
+
- uses: ruby/setup-ruby@v1
|
16
|
+
with:
|
17
|
+
ruby-version: ${{ matrix.ruby }}
|
18
|
+
bundler-cache: true
|
19
|
+
- run: gem install forward-proxy
|
20
|
+
- run: |
|
21
|
+
exe/forward-proxy --h
|
22
|
+
exe/forward-proxy --help
|
23
|
+
exe/forward-proxy --binding 127.0.0.1 --port 3001 --threads 2 &
|
24
|
+
sleep 1
|
25
|
+
curl --fail -x http://127.0.0.1:3001 https://google.com/
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,22 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## 0.2.0
|
4
|
+
|
5
|
+
- Extract errors into module.
|
6
|
+
|
7
|
+
## 0.1.5
|
8
|
+
|
9
|
+
- stream http proxy requests.
|
10
|
+
- call shutdown on `Interrupt`
|
11
|
+
- remove waiting for the thread-pool to finish on shutdown and just close the server conn.
|
12
|
+
|
13
|
+
## 0.1.4
|
14
|
+
|
15
|
+
- `accept` connections via thread pool.
|
16
|
+
- catch and throw custom http parse error.
|
17
|
+
- `Server:` header added for default error 502 response.
|
18
|
+
- rescue and logs socket error `Errno::EBADF`.
|
19
|
+
|
3
20
|
## 0.1.3
|
4
21
|
|
5
22
|
- Refine CLI help text.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
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
|
data/Rakefile
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rake/testtask"
|
3
|
+
require 'yard'
|
4
|
+
|
5
|
+
YARD::Rake::YardocTask.new do |t|
|
6
|
+
t.files = ['lib/**/*.rb', 'README', 'CHANGELOG', 'CODE_OF_CONDUCT']
|
7
|
+
t.options = []
|
8
|
+
t.stats_options = ['--list-undoc']
|
9
|
+
end
|
3
10
|
|
4
11
|
Rake::TestTask.new(:test) do |t|
|
5
12
|
t.libs << "test"
|
@@ -7,4 +14,16 @@ Rake::TestTask.new(:test) do |t|
|
|
7
14
|
t.test_files = FileList["test/**/*_test.rb"]
|
8
15
|
end
|
9
16
|
|
17
|
+
namespace :gh do
|
18
|
+
desc "Deploy yard docs to github pages"
|
19
|
+
task :pages => :yard do
|
20
|
+
`git add -f doc`
|
21
|
+
`git commit -am "update: $(date)"`
|
22
|
+
`git subtree split --prefix doc -b gh-pages`
|
23
|
+
`git push -f origin gh-pages:gh-pages`
|
24
|
+
`git branch -D gh-pages`
|
25
|
+
`git reset head~1`
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
10
29
|
task :default => :test
|
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
|
|
@@ -26,6 +26,5 @@ OptionParser.new do |parser|
|
|
26
26
|
end.parse!
|
27
27
|
|
28
28
|
require_relative '../lib/forward_proxy'
|
29
|
-
|
30
29
|
server = ForwardProxy::Server.new(options)
|
31
30
|
server.start
|
data/lib/forward_proxy.rb
CHANGED
data/lib/forward_proxy/server.rb
CHANGED
@@ -1,73 +1,90 @@
|
|
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
|
-
|
9
10
|
class Server
|
10
|
-
attr_reader :bind_address, :bind_port
|
11
|
+
attr_reader :bind_address, :bind_port, :logger
|
11
12
|
|
12
|
-
def initialize(bind_address: "127.0.0.1", bind_port: 9292, threads: 32)
|
13
|
+
def initialize(bind_address: "127.0.0.1", bind_port: 9292, threads: 32, logger: Logger.new(STDOUT, level: :info))
|
14
|
+
@logger = logger
|
13
15
|
@thread_pool = ThreadPool.new(threads)
|
14
16
|
@bind_address = bind_address
|
15
17
|
@bind_port = bind_port
|
16
18
|
end
|
17
19
|
|
18
20
|
def start
|
19
|
-
@server = TCPServer.new(bind_address, bind_port)
|
20
|
-
|
21
21
|
thread_pool.start
|
22
22
|
|
23
|
-
|
23
|
+
@socket = TCPServer.new(bind_address, bind_port)
|
24
24
|
|
25
|
-
|
26
|
-
client = server.accept
|
25
|
+
logger.info("Listening #{bind_address}:#{bind_port}")
|
27
26
|
|
28
|
-
|
27
|
+
loop do
|
28
|
+
thread_pool.schedule(socket.accept) do |client_conn|
|
29
29
|
begin
|
30
30
|
req = parse_req(client_conn)
|
31
31
|
|
32
|
-
|
32
|
+
logger.info(req.request_line)
|
33
33
|
|
34
34
|
case req.request_method
|
35
35
|
when METHOD_CONNECT then handle_tunnel(client_conn, req)
|
36
36
|
when METHOD_GET, METHOD_POST then handle(client_conn, req)
|
37
37
|
else
|
38
|
-
raise HTTPMethodNotImplemented
|
38
|
+
raise Errors::HTTPMethodNotImplemented
|
39
39
|
end
|
40
|
-
rescue =>
|
41
|
-
client_conn
|
42
|
-
HTTP/1.1 502
|
43
|
-
eos
|
44
|
-
|
45
|
-
puts e.message
|
46
|
-
puts e.backtrace.map { |line| " #{line}" }
|
40
|
+
rescue => err
|
41
|
+
handle_error(err, client_conn)
|
47
42
|
ensure
|
48
43
|
client_conn.close
|
49
44
|
end
|
50
45
|
end
|
51
46
|
end
|
52
47
|
rescue Interrupt
|
48
|
+
shutdown
|
49
|
+
rescue IOError, Errno::EBADF => e
|
50
|
+
logger.error(e.message)
|
53
51
|
end
|
54
52
|
|
55
53
|
def shutdown
|
56
|
-
|
57
|
-
|
54
|
+
if socket
|
55
|
+
logger.info("Shutting down")
|
58
56
|
|
59
|
-
|
60
|
-
|
57
|
+
socket.close
|
58
|
+
end
|
61
59
|
end
|
62
60
|
|
63
61
|
private
|
64
62
|
|
65
|
-
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
|
66
68
|
|
67
69
|
METHOD_CONNECT = "CONNECT"
|
68
70
|
METHOD_GET = "GET"
|
69
71
|
METHOD_POST = "POST"
|
70
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
|
+
|
77
|
+
# The following comments are from the IETF document
|
78
|
+
# "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content"
|
79
|
+
# https://tools.ietf.org/html/rfc7231#section-4.3.6
|
80
|
+
|
81
|
+
# A proxy MUST send an appropriate Via header field, as described
|
82
|
+
# below, in each message that it forwards. An HTTP-to-HTTP gateway
|
83
|
+
# MUST send an appropriate Via header field in each inbound request
|
84
|
+
# message and MAY send a Via header field in forwarded response
|
85
|
+
# messages.
|
86
|
+
HEADER_VIA = "HTTP/1.1 ForwardProxy"
|
87
|
+
|
71
88
|
def handle_tunnel(client_conn, req)
|
72
89
|
# The following comments are from the IETF document
|
73
90
|
# "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content"
|
@@ -89,7 +106,10 @@ module ForwardProxy
|
|
89
106
|
# blank line that concludes the successful response's header section;
|
90
107
|
# data received after that blank line is from the server identified by
|
91
108
|
# the request-target.
|
92
|
-
client_conn.write
|
109
|
+
client_conn.write <<~eos.chomp
|
110
|
+
HTTP/1.1 200 OK
|
111
|
+
#{HEADER_EOP}
|
112
|
+
eos
|
93
113
|
|
94
114
|
# The CONNECT method requests that the recipient establish a tunnel to
|
95
115
|
# the destination origin server identified by the request-target and,
|
@@ -104,22 +124,57 @@ module ForwardProxy
|
|
104
124
|
end
|
105
125
|
|
106
126
|
def handle(client_conn, req)
|
107
|
-
|
108
|
-
http.request
|
127
|
+
Net::HTTP.start(req.host, req.port) do |http|
|
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
|
+
|
149
|
+
client_conn.puts <<~eos.chomp
|
150
|
+
HTTP/1.1 #{resp.code}
|
151
|
+
#{headers.map { |header, value| "#{header}: #{value}" }.join("\n")}
|
152
|
+
#{HEADER_EOP}
|
153
|
+
eos
|
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.
|
161
|
+
resp.read_body do |chunk|
|
162
|
+
client_conn << chunk
|
163
|
+
end
|
164
|
+
end
|
109
165
|
end
|
166
|
+
end
|
110
167
|
|
111
|
-
|
112
|
-
# below, in each message that it forwards. An HTTP-to-HTTP gateway
|
113
|
-
# MUST send an appropriate Via header field in each inbound request
|
114
|
-
# message and MAY send a Via header field in forwarded response
|
115
|
-
# messages.
|
168
|
+
def handle_error(err, client_conn)
|
116
169
|
client_conn.puts <<~eos.chomp
|
117
|
-
HTTP/1.1
|
118
|
-
Via: #{
|
119
|
-
#{
|
120
|
-
|
121
|
-
#{resp.body}
|
170
|
+
HTTP/1.1 502
|
171
|
+
Via: #{HEADER_VIA}
|
172
|
+
#{HEADER_EOP}
|
122
173
|
eos
|
174
|
+
|
175
|
+
logger.error(err.message)
|
176
|
+
|
177
|
+
logger.debug(err.backtrace.join("\n"))
|
123
178
|
end
|
124
179
|
|
125
180
|
def map_webrick_to_net_http_req(req)
|
@@ -129,26 +184,24 @@ module ForwardProxy
|
|
129
184
|
when METHOD_GET then Net::HTTP::Get
|
130
185
|
when METHOD_POST then Net::HTTP::Post
|
131
186
|
else
|
132
|
-
raise HTTPMethodNotImplemented
|
187
|
+
raise Errors::HTTPMethodNotImplemented
|
133
188
|
end
|
134
189
|
|
135
190
|
klass.new(req.path, req_headers)
|
136
191
|
end
|
137
192
|
|
138
|
-
def transfer(
|
193
|
+
def transfer(src_conn, dest_conn)
|
139
194
|
IO.copy_stream(src_conn, dest_conn)
|
140
195
|
rescue => e
|
141
|
-
|
196
|
+
logger.warn(e.message)
|
142
197
|
end
|
143
198
|
|
144
199
|
def parse_req(client_conn)
|
145
200
|
WEBrick::HTTPRequest.new(WEBrick::Config::HTTP).tap do |req|
|
146
201
|
req.parse(client_conn)
|
147
202
|
end
|
148
|
-
|
149
|
-
|
150
|
-
def log(str, level = 'INFO')
|
151
|
-
puts "[#{Time.now}] #{level} #{str}"
|
203
|
+
rescue => e
|
204
|
+
throw Errors::HTTPParseError.new(e.message)
|
152
205
|
end
|
153
206
|
end
|
154
207
|
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.4.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-04 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Forward proxy using just Ruby standard libraries.
|
14
14
|
email:
|
@@ -19,6 +19,7 @@ extensions: []
|
|
19
19
|
extra_rdoc_files: []
|
20
20
|
files:
|
21
21
|
- ".github/workflows/ci.yaml"
|
22
|
+
- ".github/workflows/cli.yaml"
|
22
23
|
- ".gitignore"
|
23
24
|
- CHANGELOG.md
|
24
25
|
- CODE_OF_CONDUCT.md
|
@@ -31,6 +32,8 @@ files:
|
|
31
32
|
- exe/forward-proxy
|
32
33
|
- forward-proxy.gemspec
|
33
34
|
- lib/forward_proxy.rb
|
35
|
+
- lib/forward_proxy/errors/http_method_not_implemented.rb
|
36
|
+
- lib/forward_proxy/errors/http_parse_error.rb
|
34
37
|
- lib/forward_proxy/server.rb
|
35
38
|
- lib/forward_proxy/thread_pool.rb
|
36
39
|
- lib/forward_proxy/version.rb
|
@@ -56,8 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
59
|
- !ruby/object:Gem::Version
|
57
60
|
version: '0'
|
58
61
|
requirements: []
|
59
|
-
|
60
|
-
rubygems_version: 2.5.2.3
|
62
|
+
rubygems_version: 3.0.3
|
61
63
|
signing_key:
|
62
64
|
specification_version: 4
|
63
65
|
summary: Forward proxy.
|